The purpose of this issue is to provide a reasonable developer experience for missing reflection hints on types invisible to Spring AOT runtime hints inference. Typical use case is usage of reflection-based libraries in the implementation without exposing related types in methods APIs.
Use cases
Jackson
A popular example is serialization/deserialization with like Jackson, either directly invoked via objectMapper.readValue(json, POJO.class)
or most often via WebClient
/RestTemplate
. Unlike with @RequestMapping
return values, Spring AOT processing has no way to infer this. That means a developer will have to create a RuntimeHintsRegistrar
implementation that will leverage BindingReflectionHintsRegistrar
like here and to declare it on a bean with @ImportRuntimeHints
. It works but it is painful and verbose.
Quartz
Another example is Quartz jobs, declared in spring-aot-smoke-tests as following:
@Configuration
class QuartzConfiguration {
@Bean
public JobDetail simpleJobDetail() {
return JobBuilder.newJob(SimpleJob.class).withIdentity("simpleJob").usingJobData("greeting", "Hello world!")
.storeDurably().build();
}
// ...
}
The current solution is to create a RuntimeHintsRegistrar
implementation and to declare it on a bean with @ImportRuntimeHints
which is also pretty verbose.
Spring Native
In Spring Native, this kind of use case was handled by annotating a bean with for example @TypeHint(types = Data.class, access = { TypeAccess.DECLARED_CONSTRUCTORS, TypeAccess.PUBLIC_METHODS })
, but this was pretty hard to get it right when the reflection-based serialization required entries for multiple classes (typically the non-trivial logic implemented in SF6 BindingReflectionHintsRegistrar
).
Notice there are a bunch of FailureAnalyzer
that take this assumption that on native a missing reflection entry is likely related to missing native configuration, so the error message guide the user to try to add the right one.
The ideal fix for this issue was discussed in spring-projects-experimental/spring-native#1152 but what is proposed here sounds not realistic for Spring Framework 6 GA, so we need a more pragmatic solution.
Proposal
We could maybe introduce an annotation applied typically at bean class level that would allow the user to specify that a want reflection hints for a specific type or types specified by Class
or String
. It could apply BindingReflectionHintsRegistrar
logic (reflection on all types recursively used in the type hierarchy for setters, getters, record methods and fields) on the specified types:
@ReflectionHintsForTypeHierarchy(Foo.class)
@ReflectionHintsForTypeHierarchy({ Foo.class, Bar.class })
@ReflectionHintsForTypeHierarchy(typeNames = "com.example.Foo")
@ReflectionHintsForTypeHierarchy(typeNames = { "com.example.Foo", "com.example.Bar" })
Side notes:
- Other proposal than @ReflectionHintsForTypeHierarchy
welcomed if you have better ideas
- Maybe we could take this opportunity to rename BindingReflectionHintsRegistrar
(TypeHierarchyReflectionHintsRegistrar
?) to be consistent with this annotation and have a more generic name. "Binding" was chosen because it was less confusing than "Serialization" which can apply to both Java serialization and reflection-based serialization.
- Is there any relationship between this and @Reflective
?
Comment From: snicoll
We're recently extracted the logic of processing @Reflective
-annotated types in a reusable service rather than relying on the fact that they were placed on a bean. This had the effect of reducing the boilerplate by adding @Reflective
on the required elements and kick-off the processing by specifying the Class
. Such processing keeps being conditional so we only contribute those hints if the infrastructure that uses it is enabled.
@Reflective
is focused on reflection and ReflectionHints
. While this is a major piece it is not the only one so, perhaps, we should find a way to expand that a bit. A first step could be to find a way to expand the contract to cover RuntimeHints
.
A possible annotation reusing our infrastucture could be:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Reflective(RegisterReflectionProcessor.class)
public @interface RegisterReflection {
Class<?>[] value();
// Other fields
}
RegisterReflectionProcessor
can read the @RegisterReflection
based on the annotated element, and kick off a processing based on the attributes on the annotation.
We could have as "many" annotations and processors as we want to. BindingReflectionHintsRegistrar
should probably become an implementation detail of one of those.