-
Notifications
You must be signed in to change notification settings - Fork 38.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for providing a POJO to an initializer registered by BeanFactoryInitializationCode #32214
Comments
@Christopher-Chianelli I am afraid I don't understand what you're trying to do and what is currently missing in the core framework. Tools to generate code is available, I need to insist that you should not generate your own code using Spring AOT unless absolutely necessary. Can you try to rephrase and, in particular, what you think need to change here. |
The ability to pass an actual object instance to an initializer.
This works for code that only needs access to a beanFactory, environment, or resourceLoader. But in my particular case, I have a pre-constructed object that I need to pass to the initializer so it can register it to the beanFactory (i.e. my AOT initializer needs to do something like this): public static void registerSolverConfig(ConfigurableListableBeanFactory beanFactory, SolverConfig solverConfig) {
beanFactory.registerSingleton(DEFAULT_SOLVER_CONFIG_NAME, solverConfig);
} Since beanFactoryInitializationCode.addInitializer(new DefaultMethodReference(
MethodSpec.methodBuilder("registerSolverConfig")
.addModifiers(Modifier.PUBLIC)
.addModifiers(Modifier.STATIC)
.addParameter(ConfigurableListableBeanFactory.class, "beanFactory")
.addFixedParameter(SolverConfig.class, "solverConfig", solverConfig)
.build(), MyClass.class)); In particular, there a new method to This needs to be done as beans registered by |
@snicoll We're going to create a simple, isolated reproducer in a separate repo to demonstrate the issue clearly, out of respect for your time. Let us get back to you here when we have that. |
That would be very much appreciated @ge0ffrey as the second comment is not helping I am afraid. Before you do all of that, please keep in mind:
|
@snicoll Hello. It's me who you're quoting here. In fact, I was making the same point you are - the approach we came up with is not acceptable, and there has to be a standard approach that doesn't rely on any kind of serialization. If we can't find the correct approach ourselves, we should ask the Spring team. I was making the point that nobody should be doing serialization here. :-) Anyway, we'll work on getting this down to a minimal reproducer. |
Reproducer of why we need to do AOT processing: https://github.com/Christopher-Chianelli/issue-reproducer/tree/spring-entity-scanner-native/project |
I am so confused. So we went from code generation needs a way to provide a POJO because of some sort of serialization problem to "claspath scanning does not work". If you are figuring out things as you explore, I'd argue that it's not yet time to raise an issue here (@triceo). Ironically enough, I described what the reproducer shows in my last comment:
There's no classpath in a native image so there's no way for whatever code that does that to work. If Timefold Solver does classpath scanning then you need to do something about it. You can use PersistenceManagedTypesBeanRegistrationAotProcessor as an inspiration. If you need more help understanding how GraalVM and native image with Java work, please refer to the GraalVM support or StackOverflow. |
I am sorry to hear that the reproducer was rejected, because the EntityScanner is not designed to support native mode. We'll will evaluate the PersistenceManagedTypesBeanRegistrationAotProcessor proposal. The issue consists out of multiple issues, the EntityScanner reproducer isolated only step 1. Sorry for the confusion that mixing multiple issues brought. In essence (in the outer issue), for performance reasons we want to read the solverConfig.xml at build time into a javabean (SolverConfig) and pass that to the runtime (not just in native mode, but also in non-native mode), to avoid parsing XML at bootstrap time. For serverless deployments. In Quakus we do this through their Recorder class, which takes care of all the boilerplate code for us. Currently we got Spring Native working 🎉 , although we serialize and deserialize (=parse) the config to pass it from build time to bootstrap time. See https://github.com/TimefoldAI/timefold-solver/pull/609/files We 'll run some benchmarks what's the startup cost for this and welcome ideas how to improve this going forward. Rome wasn't build in a day :) |
I think you misunderstood why the issue was closed. The issue was closed because it isn't actionable. We don't use our issue tracker as a support forum and this issue clearly looks like one to me. As for
That's totally doable, and even more so if you want to do that in both JVM and native mode. That's said, Spring does not require any build-time transformation on the JVM at all. Ignoring that, and focusing on AOT, there's nothing I can see from that issue that prevents doing that. If we want to make progress, you could create an issue describing what you'd like to happen, potentially linking to what Quarkus does for reference. |
For what it is worth, I think the actual issue is upstream from Spring; I thought MyObject1 a = new MyObject1(...);
MyObject2 b = new MyObject2(...);
recorder.myMethod(a, b); and it generates this code either inside a static initializer or the main method MyObject1 a = new MyObject1();
a.setFieldA(...);
a.setFieldB(...);
MyObject2 b = new MyObject2();
b.setFieldA(...);
b.setFieldB(...);
recorder.myMethod(a, b); So what I wanted to see from Spring was a way to do this in a way that was as easy as Quarkus. ObjectInliner.getDefault().trustEverything();
CodeBlock.of("$T.$N($V, $V)", MyRecord.class, "methodName", new MyObject1(...). new MyObject2(...)); which is transformed into something like this: MyRecord.methodName(
((MyObject1)((java.util.function.Supplier)(() -> {
MyComplexConfig $$javapoet$MyObject1 = new MyObject1();
$$javapoet$MyObject1.setParameter1(1);
$$javapoet$MyObject1.setParameter2("abc");
// ...
return $$javapoet$MyObject1;
})).get()),
((MyObject2)((java.util.function.Supplier)(() -> {
MyComplexConfig $$javapoet$MyObject2 = new MyObject2();
$$javapoet$MyObject2.setParameter1(1);
$$javapoet$MyObject2.setParameter2("abc");
// ...
return $$javapoet$MyObject2;
})).get())
); (the generated code might be ugly, but allows the value to be put inside a fragment/incomplete block). |
As for why I thought deserialization did not work in generated code, it because if you register Class.class for reflection, any usage of ObjectMapper or XML deserialization will cause a native image error (see https://github.com/Christopher-Chianelli/issue-reproducer/tree/spring-aot-cannot-pass-read-xml/project ). Class.class was register for reflection since I was registered the types needed for reflection recursively (i.e. like this): private static void registerForReflection(ReflectionHints reflectionHints, Class<?> type, Set<Class<?>> visited) {
if (type == null || visited.contains(type)) {
return;
}
visited.add(type);
reflectionHints.registerType(type,
MemberCategory.INTROSPECT_PUBLIC_METHODS,
MemberCategory.INTROSPECT_DECLARED_METHODS,
MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS,
MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS,
MemberCategory.PUBLIC_FIELDS,
MemberCategory.DECLARED_FIELDS,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS,
MemberCategory.INVOKE_DECLARED_METHODS,
MemberCategory.INVOKE_PUBLIC_METHODS);
for (Field field : type.getDeclaredFields()) {
registerForReflection(reflectionHints, field.getType(), visited);
}
registerForReflection(reflectionHints, type.getSuperclass(), visited);
} This is needed because for XML/JSON deserialization to work, the type of the fields of the root type must also be registered for reflection (i.e. it not enough to register A.class for reflection if it contains instances of B.class; both A.class and B.class need to be registered for reflection). As long as you don't register Class.class or ClassLoader.class for reflection (i.e. by checking for them in the if), you can use ObjectMapper in generated code (to use JAXB currently requires additional work, see https://stackoverflow.com/questions/77969978/how-to-use-jaxb-in-native-image-without-using-agent) |
Ok. We have a working way to do Spring Native support in Timefold, so all is well :) If you think it's valuable us to extract latest Christopher's comments into clean, separate RFE issues, we're happy to do that to help out. But for us it's also fine to ignore them, as these are not blockers on our side and we are happy with what we have now: Spring Native works. Thank you for your time, Stéphane. Much appreciated :) |
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 theSolverConfig
to theConfigurableListableBeanFactory
. Inside native mode, this is done by aBeanFactoryInitializationAotProcessor
, which generates a class with the generatedSolverConfig
and adds an initializer (that uses the generated class) that registers the SolverConfig to theConfigurableListableBeanFactory
.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:but this causes a ZipFile object to appear in the image heap, making native image compilation fail:
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
To inline
BasicPojo myField = new BasicPojo(new BasicPojo(null, 0, "parent"), 1, "child")
, it would generate the following code:What I would want to do instead is something like this:
which would be translated to something like this:
The text was updated successfully, but these errors were encountered: