Skip to content

Provide better DevXP for missing reflection hints where inference is not possible #28979

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

Closed
sdeleuze opened this issue Aug 18, 2022 · 1 comment
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) theme: aot An issue related to Ahead-of-time processing
Milestone

Comments

@sdeleuze
Copy link
Contributor

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-attic/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?
@sdeleuze sdeleuze added in: core Issues in core modules (aop, beans, core, context, expression) theme: aot An issue related to Ahead-of-time processing labels Aug 18, 2022
@sdeleuze sdeleuze added this to the 6.0.0-M6 milestone Aug 18, 2022
@sdeleuze sdeleuze self-assigned this Aug 18, 2022
@snicoll
Copy link
Member

snicoll commented Aug 19, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) theme: aot An issue related to Ahead-of-time processing
Projects
None yet
Development

No branches or pull requests

2 participants