-
Notifications
You must be signed in to change notification settings - Fork 38.6k
Description
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()));