Skip to content

Add support for providing a POJO to an initializer registered by BeanFactoryInitializationCode #32214

@Christopher-Chianelli

Description

@Christopher-Chianelli

I am adding support for Spring Boot native image for the Timefold Solver: TimefoldAI/timefold-solver#609. One of the things we do ahead of time is generate the SolverConfig, a POJO (a class with getter and setter for all fields (including inherited fields), who field types are all also POJOs (or a primitive/builtin/collection type)).

The Timefold Solver needs to register the generated SolverConfig as a bean, since it is used when constructing the other beans Timefold Solver provides (SolverFactory, SolverManager, etc). Outside of native mode, this is done by a BeanFactoryPostProcessor, which directly registers the SolverConfig to the ConfigurableListableBeanFactory. Inside native mode, this is done by a BeanFactoryInitializationAotProcessor, which generates a class with the generated SolverConfig and adds an initializer (that uses the generated class) that registers the SolverConfig to the ConfigurableListableBeanFactory.

The issue is, SolverConfig has many different fields, and have multiple configurations nested within it. This make it non-trivial to put inside a generated class. Normally, I would use XML serialization/deserialization to put it inside the generated class:

SolverConfigIO solverConfigIO = new SolverConfigIO();
for (Map.Entry<String, SolverConfig> solverConfigEntry : solverConfigMap.entrySet()) {
        StringWriter writer = new StringWriter();
        solverConfigIO.write(solverConfigEntry.getValue(), writer);
        solverConfigToXml.put(solverConfigEntry.getKey(), writer.toString());
}

GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeature("timefold-aot",
    builder -> {
        final String SOLVER_CONFIG_MAP_FIELD = "solverConfigMap";
        builder.addField(Map.class, SOLVER_CONFIG_MAP_FIELD, Modifier.STATIC);
        CodeBlock.Builder initializer = CodeBlock.builder();
        initializer.add("$L = new $T();\n", SOLVER_CONFIG_MAP_FIELD, HashMap.class);
        initializer.add("$T solverConfigIO = new $T();\n", SolverConfigIO.class, SolverConfigIO.class);
        for (Map.Entry<String, String> solverConfigXmlEntry : solverConfigToXml.entrySet()) {
            initializer.add("$L.put($S, solverConfigIO.read(new $T($S)));\n", SOLVER_CONFIG_MAP_FIELD,
            solverConfigXmlEntry.getKey(),
            StringReader.class,
            solverConfigXmlEntry.getValue());
        }
        builder.addStaticBlock(initializer.build());
        // ...
  });

but this causes a ZipFile object to appear in the image heap, making native image compilation fail:

Trace: Object was reached by
  reading field jdk.internal.module.ModuleReferences$JarModuleReader.jf of constant 
    jdk.internal.module.ModuleReferences$JarModuleReader@498e3ac2: jdk.internal.module.ModuleReferences$JarModuleReader@498e3ac2
  reading field java.util.concurrent.ConcurrentHashMap$Node.val of constant 
    java.util.concurrent.ConcurrentHashMap$Node@17d8a5ed: [module org.graalvm.nativeimage.objectfile, location=...
  indexing into array java.util.concurrent.ConcurrentHashMap$Node[]@4776bc5a: [Ljava.util.concurrent.ConcurrentHashMap$Node;@4776bc5a
  reading field java.util.concurrent.ConcurrentHashMap.table of constant 
    java.util.concurrent.ConcurrentHashMap@e71ccfd: {[module org.graalvm.nativeimage.base, location=file:...
  reading field jdk.internal.loader.BuiltinClassLoader.moduleToReader of constant 
    jdk.internal.loader.ClassLoaders$AppClassLoader@c818063: jdk.internal.loader.ClassLoaders$AppClassLoader@c818063
  reading field com.oracle.svm.core.hub.DynamicHubCompanion.classLoader of constant 
    com.oracle.svm.core.hub.DynamicHubCompanion@1027bef: com.oracle.svm.core.hub.DynamicHubCompanion@1027bef
  reading field java.lang.Class.companion of constant 
    java.lang.Class@76e6eb32: class com.oracle.svm.core.code.CodeInfoDecoderCounters
  manually triggered rescan

To workaround this, I basically needed to write my own POJO serializer that serializes an arbitrary POJO into the static initialization block of the generated class: https://github.com/Christopher-Chianelli/timefold-solver/blob/spring-boot-native/spring-integration/spring-boot-autoconfigure/src/main/java/ai/timefold/solver/spring/boot/autoconfigure/util/PojoInliner.java. For instance, given

public class BasicPojo {
    BasicPojo parentPojo;
    int id;
    String name;

    public BasicPojo() {
    }

    public BasicPojo(BasicPojo parentPojo, int id, String name) {
        this.parentPojo = parentPojo;
        this.id = id;
        this.name = name;
    }

    public BasicPojo getParentPojo() {
        return parentPojo;
    }

    public void setParentPojo(BasicPojo parentPojo) {
        this.parentPojo = parentPojo;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

To inline BasicPojo myField = new BasicPojo(new BasicPojo(null, 0, "parent"), 1, "child"), it would generate the following code:

static BasicPojo myField;
static {
    Map $pojoMap = new HashMap();
    BasicPojo $obj0;
    $obj0 = new BasicPojo();
    $pojoMap.put("$obj0", $obj0);
    $obj0.setId(1);
    $obj0.setName("child");
    BasicPojo $obj1;
    $obj1 = new BasicPojo();
    $pojoMap.put("$obj1", $obj1);
    $obj1.setId(0);
    $obj1.setName("parent");
    $obj1.setParentPojo(null);
    $obj0.setParentPojo(((BasicPojo) $pojoMap.get("$obj1")));
    myField = $obj0;
}

What I would want to do instead is something like this:

static public void registerSolverConfigs(Environment environment, ConfigurableListableBeanFactory beanFactory, Map<String, SolverConfig> solverConfigMap) {
    // ...
}
// ...
beanFactoryInitializationCode.addInitializer(new DefaultMethodReference(
            MethodSpec.methodBuilder("registerSolverConfigs")
                    .addModifiers(Modifier.PUBLIC)
                    .addModifiers(Modifier.STATIC)
                    .addParameter(Environment.class, "environment")
                    .addParameter(ConfigurableListableBeanFactory.class, "beanFactory")
                    .addFixedParameter(Map.class, "solverConfigMap", solverConfigMap)
                    .build(), MyClass.getName()));

which would be translated to something like this:

GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeature("timefold-aot",
        builder -> {
            PojoInliner.inlineFields(builder,
              PojoInliner.field(Map.class, "solverConfigMap", solverConfigMap)
            );
            CodeBlock.Builder registerSolverConfigsMethod = CodeBlock.builder();
            registerSolverConfigsMethod.add("$T.$L($L, $L, $L);", MyClass.class, "registerSolverConfigs",  "environment",  "beanFactory", "solverConfigMap");
            builder.addMethod(MethodSpec.methodBuilder("registerSolverConfigs")
                .addModifiers(Modifier.PUBLIC)
                .addModifiers(Modifier.STATIC)
                .addParameter(Environment.class, "environment")
                .addParameter(ConfigurableListableBeanFactory.class, "beanFactory")
                .addCode(registerSolverConfigsMethod.build())
                .build());

            builder.build();
});

beanFactoryInitializationCode.addInitializer(new DefaultMethodReference(
    MethodSpec.methodBuilder("registerSolverConfigs")
        .addModifiers(Modifier.PUBLIC)
        .addModifiers(Modifier.STATIC)
        .addParameter(Environment.class, "environment")
        .addParameter(ConfigurableListableBeanFactory.class, "beanFactory")
        .build(), generatedClass.getName()));

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: invalidAn issue that we don't feel is valid

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions