diff --git a/framework-docs/modules/ROOT/pages/core/aot.adoc b/framework-docs/modules/ROOT/pages/core/aot.adoc index 08969064f73..0c349afe264 100644 --- a/framework-docs/modules/ROOT/pages/core/aot.adoc +++ b/framework-docs/modules/ROOT/pages/core/aot.adoc @@ -509,12 +509,19 @@ It is also possible to register an implementation statically by adding an entry {spring-framework-api}/aot/hint/annotation/Reflective.html[`@Reflective`] provides an idiomatic way to flag the need for reflection on an annotated element. For instance, `@EventListener` is meta-annotated with `@Reflective` since the underlying implementation invokes the annotated method using reflection. -By default, only Spring beans are considered, and an invocation hint is registered for the annotated element. -This can be tuned by specifying a custom `ReflectiveProcessor` implementation via the -`@Reflective` annotation. +Out-of-the-box, only Spring beans are considered but you can opt-in for scanning using `@ReflectiveScan`. +In the example below, all types of the package `com.example.app` and their subpackages are considered: + +include-code::./MyConfiguration[] + +Scanning happens during AOT processing and the types in the target packages do not need to have a class-level annotation to be considered. +This performs a "deep scan" and the presence of `@Reflective`, either directly or as a meta-annotation, is checked on types, fields, constructors, methods, and enclosed elements. + +By default, `@Reflective` registers an invocation hint for the annotated element. +This can be tuned by specifying a custom `ReflectiveProcessor` implementation via the `@Reflective` annotation. Library authors can reuse this annotation for their own purposes. -If components other than Spring beans need to be processed, a `BeanFactoryInitializationAotProcessor` can detect the relevant types and use `ReflectiveRuntimeHintsRegistrar` to process them. +An example of such customization is covered in the next section. [[aot.hints.register-reflection]] @@ -522,11 +529,13 @@ If components other than Spring beans need to be processed, a `BeanFactoryInitia {spring-framework-api}/aot/hint/annotation/RegisterReflection.html[`@RegisterReflection`] is a specialization of `@Reflective` that provides a declarative way of registering reflection for arbitrary types. +NOTE: As a specialization of `@Reflective`, this is also detected if you're using `@ReflectiveScan`. + In the following example, public constructors and public methods can be invoked via reflection on `AccountService`: include-code::./MyConfiguration[tag=snippet,indent=0] -`@RegisterReflection` can be applied to any Spring bean at the class level, but it can also be applied directly to a method to better indicate where the hints are actually required. +`@RegisterReflection` can be applied to any target type at the class level, but it can also be applied directly to a method to better indicate where the hints are actually required. `@RegisterReflection` can be used as a meta-annotation to provide more specific needs. {spring-framework-api}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`] is such composed annotation and registers the need for serializing arbitrary types. diff --git a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/reflective/MyConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/reflective/MyConfiguration.java new file mode 100644 index 00000000000..109101f41b7 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/reflective/MyConfiguration.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.core.aot.hints.reflective; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ReflectiveScan; + +@Configuration +@ReflectiveScan("com.example.app") +public class MyConfiguration { +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java index 97065f5dc2a..002355b8c25 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java @@ -22,6 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; /** @@ -61,9 +62,8 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar; * @author Brian Clozel * @author Stephane Nicoll * @since 6.0 - * @see org.springframework.aot.hint.RuntimeHints - * @see org.springframework.aot.hint.annotation.Reflective - * @see org.springframework.aot.hint.annotation.RegisterReflection + * @see RuntimeHints + * @see ReflectiveScan @ReflectiveScan */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ReflectiveScan.java b/spring-context/src/main/java/org/springframework/context/annotation/ReflectiveScan.java new file mode 100644 index 00000000000..efff134d6dd --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ReflectiveScan.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.aot.hint.annotation.Reflective; +import org.springframework.aot.hint.annotation.RegisterReflection; +import org.springframework.core.annotation.AliasFor; + +/** + * Scan arbitrary types for use of {@link Reflective}. Typically used on + * {@link Configuration @Configuration} classes but can be added to any bean. + * Scanning happens during AOT processing, typically at build-time. + * + *

In the example below, {@code com.example.app} and its subpackages are + * scanned:


+ * @Configuration
+ * @ReflectiveScan("com.example.app")
+ * class MyConfiguration {
+ *     // ...
+ * }
+ * + *

Either {@link #basePackageClasses} or {@link #basePackages} (or its alias + * {@link #value}) may be specified to define specific packages to scan. If specific + * packages are not defined, scanning will occur recursively beginning with the + * package of the class that declares this annotation. + * + *

A type does not need to be annotated at class level to be candidate, and + * this performs a "deep scan" by loading every class in the target packages and + * search for {@link Reflective} on types, constructors, methods, and fields. + * Enclosed classes are candidates as well. Classes that fail to load are + * ignored. + * + * @author Stephane Nicoll + * @see Reflective @Reflective + * @see RegisterReflection @RegisterReflection + * @since 6.2 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +public @interface ReflectiveScan { + + /** + * Alias for {@link #basePackages}. + *

Allows for more concise annotation declarations if no other attributes + * are needed — for example, {@code @ReflectiveScan("org.my.pkg")} + * instead of {@code @ReflectiveScan(basePackages = "org.my.pkg")}. + */ + @AliasFor("basePackages") + String[] value() default {}; + + /** + * Base packages to scan for reflective usage. + *

{@link #value} is an alias for (and mutually exclusive with) this + * attribute. + *

Use {@link #basePackageClasses} for a type-safe alternative to + * String-based package names. + */ + @AliasFor("value") + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages} for specifying the packages + * to scan for reflection usage. The package of each class specified will be scanned. + *

Consider creating a special no-op marker class or interface in each package + * that serves no purpose other than being referenced by this attribute. + */ + Class[] basePackageClasses() default {}; + +} diff --git a/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilder.java b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilder.java new file mode 100644 index 00000000000..4b6a7b79182 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilder.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.aot; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.StreamSupport; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.annotation.Reflective; +import org.springframework.aot.hint.annotation.ReflectiveProcessor; +import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; +import org.springframework.aot.hint.annotation.RegisterReflection; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Builder for an {@linkplain BeanFactoryInitializationAotContribution AOT + * contribution} that detects the presence of {@link Reflective @Reflective} on + * annotated elements and invoke the underlying {@link ReflectiveProcessor} + * implementations. + * + *

Candidates can be provided explicitly or by scanning the classpath. + * + * @author Stephane Nicoll + * @since 6.2 + * @see Reflective + * @see RegisterReflection + */ +public class ReflectiveProcessorAotContributionBuilder { + + private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar(); + + private final Set> classes = new LinkedHashSet<>(); + + + /** + * Process the given classes by checking the ones that use {@link Reflective}. + *

A class is candidate if it uses {@link Reflective} directly or via a + * meta-annotation. Type, fields, constructors, methods and enclosed types + * are inspected. + * @param classes the classes to inspect + */ + public ReflectiveProcessorAotContributionBuilder withClasses(Iterable> classes) { + this.classes.addAll(StreamSupport.stream(classes.spliterator(), false) + .filter(registrar::isCandidate).toList()); + return this; + } + + /** + * Process the given classes by checking the ones that use {@link Reflective}. + *

A class is candidate if it uses {@link Reflective} directly or via a + * meta-annotation. Type, fields, constructors, methods and enclosed types + * are inspected. + * @param classes the classes to inspect + */ + public ReflectiveProcessorAotContributionBuilder withClasses(Class[] classes) { + return withClasses(Arrays.asList(classes)); + } + + /** + * Scan the given {@code packageNames} and their sub-packages for classes + * that uses {@link Reflective}. + *

This performs a "deep scan" by loading every class in the specified + * packages and search for {@link Reflective} on types, constructors, methods, + * and fields. Enclosed classes are candidates as well. Classes that fail to + * load are ignored. + * @param classLoader the classloader to use + * @param packageNames the package names to scan + */ + public ReflectiveProcessorAotContributionBuilder scan(@Nullable ClassLoader classLoader, String... packageNames) { + ReflectiveClassPathScanner scanner = new ReflectiveClassPathScanner(classLoader); + return withClasses(scanner.scan(packageNames)); + } + + @Nullable + public BeanFactoryInitializationAotContribution build() { + return (!this.classes.isEmpty() ? new AotContribution(this.classes) : null); + } + + private static class AotContribution implements BeanFactoryInitializationAotContribution { + + private final Class[] classes; + + public AotContribution(Set> classes) { + this.classes = classes.toArray(Class[]::new); + } + + @Override + public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { + RuntimeHints runtimeHints = generationContext.getRuntimeHints(); + registrar.registerRuntimeHints(runtimeHints, this.classes); + } + + } + + private static class ReflectiveClassPathScanner extends ClassPathScanningCandidateComponentProvider { + + @Nullable + private final ClassLoader classLoader; + + ReflectiveClassPathScanner(@Nullable ClassLoader classLoader) { + super(false); + this.classLoader = classLoader; + addIncludeFilter((metadataReader, metadataReaderFactory) -> true); + } + + Class[] scan(String... packageNames) { + if (logger.isDebugEnabled()) { + logger.debug("Scanning all types for reflective usage from " + Arrays.toString(packageNames)); + } + Set candidates = new HashSet<>(); + for (String packageName : packageNames) { + candidates.addAll(findCandidateComponents(packageName)); + } + return candidates.stream().map(c -> (Class) c.getAttribute("type")).toArray(Class[]::new); + } + + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + String className = beanDefinition.getBeanClassName(); + if (className != null) { + try { + Class type = ClassUtils.forName(className, this.classLoader); + beanDefinition.setAttribute("type", type); + return registrar.isCandidate(type); + } + catch (Exception ex) { + if (logger.isTraceEnabled()) { + logger.trace("Ignoring '%s' for reflective usage: %s".formatted(className, ex.getMessage())); + } + } + } + return false; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java index 9cba020aef5..0ba81ec9713 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java @@ -17,17 +17,20 @@ package org.springframework.context.aot; import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; -import org.springframework.aot.generate.GenerationContext; -import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.annotation.Reflective; import org.springframework.aot.hint.annotation.ReflectiveProcessor; -import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; -import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.context.annotation.ReflectiveScan; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; /** * AOT {@code BeanFactoryInitializationAotProcessor} that detects the presence @@ -39,32 +42,38 @@ import org.springframework.beans.factory.support.RegisteredBean; */ class ReflectiveProcessorBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor { - private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar(); - - @Override + @Nullable public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { - Class[] beanTypes = Arrays.stream(beanFactory.getBeanDefinitionNames()) + Class[] beanClasses = Arrays.stream(beanFactory.getBeanDefinitionNames()) .map(beanName -> RegisteredBean.of(beanFactory, beanName).getBeanClass()) .toArray(Class[]::new); - return new ReflectiveProcessorBeanFactoryInitializationAotContribution(beanTypes); + String[] packagesToScan = findBasePackagesToScan(beanClasses); + return new ReflectiveProcessorAotContributionBuilder().withClasses(beanClasses) + .scan(beanFactory.getBeanClassLoader(), packagesToScan).build(); } - - private static class ReflectiveProcessorBeanFactoryInitializationAotContribution - implements BeanFactoryInitializationAotContribution { - - private final Class[] types; - - public ReflectiveProcessorBeanFactoryInitializationAotContribution(Class[] types) { - this.types = types; + protected String[] findBasePackagesToScan(Class[] beanClasses) { + Set basePackages = new LinkedHashSet<>(); + for (Class beanClass : beanClasses) { + ReflectiveScan reflectiveScan = AnnotatedElementUtils.getMergedAnnotation(beanClass, ReflectiveScan.class); + if (reflectiveScan != null) { + basePackages.addAll(extractBasePackages(reflectiveScan, beanClass)); + } } + return basePackages.toArray(new String[0]); + } - @Override - public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { - RuntimeHints runtimeHints = generationContext.getRuntimeHints(); - registrar.registerRuntimeHints(runtimeHints, this.types); + private Set extractBasePackages(ReflectiveScan annotation, Class declaringClass) { + Set basePackages = new LinkedHashSet<>(); + Collections.addAll(basePackages, annotation.basePackages()); + for (Class clazz : annotation.basePackageClasses()) { + basePackages.add(ClassUtils.getPackageName(clazz)); + } + if (basePackages.isEmpty()) { + basePackages.add(ClassUtils.getPackageName(declaringClass)); } + return basePackages; } } diff --git a/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilderTests.java b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilderTests.java new file mode 100644 index 00000000000..5e08fafbf6b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilderTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.aot; + +import java.util.List; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.api.ObjectArrayAssert; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.context.testfixture.context.aot.scan.noreflective.ReflectiveNotUsed; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnConstructor; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnField; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnInnerField; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnInterface; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnMethod; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnNestedType; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnRecord; +import org.springframework.context.testfixture.context.aot.scan.reflective.ReflectiveOnType; +import org.springframework.context.testfixture.context.aot.scan.reflective2.Reflective2OnType; +import org.springframework.context.testfixture.context.aot.scan.reflective2.reflective21.Reflective21OnType; +import org.springframework.context.testfixture.context.aot.scan.reflective2.reflective22.Reflective22OnType; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReflectiveProcessorAotContributionBuilder}. + * + * @author Stephane Nicoll + */ +class ReflectiveProcessorAotContributionBuilderTests { + + @Test + void classesWithMatchingCandidates() { + BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder() + .withClasses(List.of(String.class, ReflectiveOnInterface.class, Integer.class)).build(); + assertDetectedClasses(contribution).containsOnly(ReflectiveOnInterface.class).hasSize(1); + } + + @Test + void classesWithMatchingCandidatesFiltersDuplicates() { + BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder() + .withClasses(List.of(ReflectiveOnField.class, ReflectiveOnInterface.class, Integer.class)) + .withClasses(new Class[] { ReflectiveOnInterface.class, ReflectiveOnMethod.class, String.class }) + .build(); + assertDetectedClasses(contribution) + .containsOnly(ReflectiveOnInterface.class, ReflectiveOnField.class, ReflectiveOnMethod.class) + .hasSize(3); + } + + @Test + void scanWithMatchingCandidates() { + String packageName = ReflectiveOnType.class.getPackageName(); + BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder() + .scan(getClass().getClassLoader(), packageName).build(); + assertDetectedClasses(contribution).containsOnly(ReflectiveOnType.class, ReflectiveOnInterface.class, + ReflectiveOnRecord.class, ReflectiveOnField.class, ReflectiveOnConstructor.class, + ReflectiveOnMethod.class, ReflectiveOnNestedType.Nested.class, ReflectiveOnInnerField.Inner.class); + } + + @Test + void scanWithMatchingCandidatesInSubPackages() { + String packageName = Reflective2OnType.class.getPackageName(); + BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder() + .scan(getClass().getClassLoader(), packageName).build(); + assertDetectedClasses(contribution).containsOnly(Reflective2OnType.class, + Reflective21OnType.class, Reflective22OnType.class); + } + + @Test + void scanWithNoCandidate() { + String packageName = ReflectiveNotUsed.class.getPackageName(); + BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder() + .scan(getClass().getClassLoader(), packageName).build(); + assertThat(contribution).isNull(); + } + + @Test + void classesAndScanWithDuplicatesFiltersThem() { + BeanFactoryInitializationAotContribution contribution = new ReflectiveProcessorAotContributionBuilder() + .withClasses(List.of(ReflectiveOnField.class, ReflectiveOnInterface.class, Integer.class)) + .withClasses(new Class[] { ReflectiveOnInterface.class, ReflectiveOnMethod.class, String.class }) + .scan(null, ReflectiveOnType.class.getPackageName()) + .build(); + assertDetectedClasses(contribution) + .containsOnly(ReflectiveOnType.class, ReflectiveOnInterface.class, ReflectiveOnRecord.class, + ReflectiveOnField.class, ReflectiveOnConstructor.class, ReflectiveOnMethod.class, + ReflectiveOnNestedType.Nested.class, ReflectiveOnInnerField.Inner.class) + .hasSize(8); + } + + @SuppressWarnings("rawtypes") + private ObjectArrayAssert assertDetectedClasses(@Nullable BeanFactoryInitializationAotContribution contribution) { + assertThat(contribution).isNotNull(); + return assertThat(contribution).extracting("classes", InstanceOfAssertFactories.array(Class[].class)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessorTests.java index 950950eed9c..dedaa501ce1 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessorTests.java @@ -21,6 +21,8 @@ import java.lang.reflect.Constructor; import org.junit.jupiter.api.Test; import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.TypeHint; +import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.annotation.Reflective; import org.springframework.aot.hint.predicate.ReflectionHintsPredicates; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; @@ -30,6 +32,10 @@ import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContrib import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.ReflectiveScan; +import org.springframework.context.testfixture.context.aot.scan.reflective2.Reflective2OnType; +import org.springframework.context.testfixture.context.aot.scan.reflective2.reflective21.Reflective21OnType; +import org.springframework.context.testfixture.context.aot.scan.reflective2.reflective22.Reflective22OnType; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -68,6 +74,48 @@ class ReflectiveProcessorBeanFactoryInitializationAotProcessorTests { .accepts(this.generationContext.getRuntimeHints()); } + @Test + void shouldTriggerScanningIfBeanUsesReflectiveScan() { + process(SampleBeanWithReflectiveScan.class); + assertThat(this.generationContext.getRuntimeHints().reflection().typeHints().map(TypeHint::getType)) + .containsExactlyInAnyOrderElementsOf(TypeReference.listOf( + Reflective2OnType.class, Reflective21OnType.class, Reflective22OnType.class)); + } + + @Test + void findBasePackagesToScanWhenNoCandidateIsEmpty() { + Class[] candidates = { String.class }; + assertThat(this.processor.findBasePackagesToScan(candidates)).isEmpty(); + } + + @Test + void findBasePackagesToScanWithBasePackageClasses() { + Class[] candidates = { SampleBeanWithReflectiveScan.class }; + assertThat(this.processor.findBasePackagesToScan(candidates)) + .containsOnly(Reflective2OnType.class.getPackageName()); + } + + @Test + void findBasePackagesToScanWithBasePackages() { + Class[] candidates = { SampleBeanWithReflectiveScanWithName.class }; + assertThat(this.processor.findBasePackagesToScan(candidates)) + .containsOnly(Reflective2OnType.class.getPackageName()); + } + + @Test + void findBasePackagesToScanWithBasePackagesAndClasses() { + Class[] candidates = { SampleBeanWithMultipleReflectiveScan.class }; + assertThat(this.processor.findBasePackagesToScan(candidates)) + .containsOnly(Reflective21OnType.class.getPackageName(), Reflective22OnType.class.getPackageName()); + } + + @Test + void findBasePackagesToScanWithDuplicatesFiltersThem() { + Class[] candidates = { SampleBeanWithReflectiveScan.class, SampleBeanWithReflectiveScanWithName.class }; + assertThat(this.processor.findBasePackagesToScan(candidates)) + .containsOnly(Reflective2OnType.class.getPackageName()); + } + private void process(Class... beanClasses) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); for (Class beanClass : beanClasses) { @@ -103,4 +151,17 @@ class ReflectiveProcessorBeanFactoryInitializationAotProcessorTests { } + @ReflectiveScan(basePackageClasses = Reflective2OnType.class) + static class SampleBeanWithReflectiveScan { + } + + @ReflectiveScan("org.springframework.context.testfixture.context.aot.scan.reflective2") + static class SampleBeanWithReflectiveScanWithName { + } + + @ReflectiveScan(basePackageClasses = Reflective22OnType.class, + basePackages = "org.springframework.context.testfixture.context.aot.scan.reflective2.reflective21") + static class SampleBeanWithMultipleReflectiveScan { + } + } diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/noreflective/ReflectiveNotUsed.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/noreflective/ReflectiveNotUsed.java new file mode 100644 index 00000000000..5c0a2f9a7c2 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/noreflective/ReflectiveNotUsed.java @@ -0,0 +1,21 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.aot.scan.noreflective; + +@SuppressWarnings("unused") +public class ReflectiveNotUsed { +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/Dummy.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/Dummy.java new file mode 100644 index 00000000000..cb7730a280a --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/Dummy.java @@ -0,0 +1,21 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.aot.scan.reflective; + +@SuppressWarnings("unused") +public class Dummy { +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnConstructor.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnConstructor.java new file mode 100644 index 00000000000..7d2cd1436db --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnConstructor.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.aot.scan.reflective; + +import org.springframework.aot.hint.annotation.Reflective; + +@SuppressWarnings("unused") +public class ReflectiveOnConstructor { + + private final String name; + + public ReflectiveOnConstructor(String name) { + this.name = name; + } + + @Reflective + public ReflectiveOnConstructor(String firstName, String lastName) { + this("%s %s".formatted(firstName, lastName)); + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnField.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnField.java new file mode 100644 index 00000000000..b143398ccd6 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnField.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.aot.scan.reflective; + +import org.springframework.aot.hint.annotation.Reflective; + +@SuppressWarnings("unused") +public class ReflectiveOnField { + + private String name; + + @Reflective + private String description; + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnInnerField.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnInnerField.java new file mode 100644 index 00000000000..272ee0617c4 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnInnerField.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.aot.scan.reflective; + +import org.springframework.aot.hint.annotation.Reflective; + +@SuppressWarnings("unused") +public class ReflectiveOnInnerField { + + public class Inner { + + @Reflective + private String description; + + } +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnInterface.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnInterface.java new file mode 100644 index 00000000000..2c0fd2f229e --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnInterface.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.aot.scan.reflective; + +import org.springframework.aot.hint.annotation.Reflective; + +@Reflective +@SuppressWarnings("unused") +public interface ReflectiveOnInterface { +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnMethod.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnMethod.java new file mode 100644 index 00000000000..ef555947a7b --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnMethod.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.aot.scan.reflective; + +import org.springframework.aot.hint.annotation.Reflective; + +@SuppressWarnings("unused") +public class ReflectiveOnMethod { + + @Reflective + void doReflection() { } +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnNestedType.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnNestedType.java new file mode 100644 index 00000000000..8441ced28d8 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnNestedType.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.aot.scan.reflective; + +import org.springframework.aot.hint.annotation.Reflective; + +@SuppressWarnings("unused") +public class ReflectiveOnNestedType { + + @Reflective + public static class Nested {} +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnRecord.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnRecord.java new file mode 100644 index 00000000000..f6d810c8b6e --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnRecord.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.aot.scan.reflective; + +import org.springframework.aot.hint.annotation.Reflective; + +@Reflective +@SuppressWarnings("unused") +public record ReflectiveOnRecord() { +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnType.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnType.java new file mode 100644 index 00000000000..9efcbbe861b --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnType.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.aot.scan.reflective; + +import org.springframework.aot.hint.annotation.Reflective; + +@Reflective +@SuppressWarnings("unused") +public class ReflectiveOnType { +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective2/Reflective2OnType.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective2/Reflective2OnType.java new file mode 100644 index 00000000000..e8839c3dc8d --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective2/Reflective2OnType.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.aot.scan.reflective2; + +import org.springframework.aot.hint.annotation.Reflective; + +@Reflective +@SuppressWarnings("unused") +public class Reflective2OnType { +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective2/reflective21/Reflective21OnType.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective2/reflective21/Reflective21OnType.java new file mode 100644 index 00000000000..fc96ffdf30e --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective2/reflective21/Reflective21OnType.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.aot.scan.reflective2.reflective21; + +import org.springframework.aot.hint.annotation.Reflective; + +@Reflective +@SuppressWarnings("unused") +public class Reflective21OnType { +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective2/reflective22/Reflective22OnType.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective2/reflective22/Reflective22OnType.java new file mode 100644 index 00000000000..e414529d1ea --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective2/reflective22/Reflective22OnType.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.testfixture.context.aot.scan.reflective2.reflective22; + +import org.springframework.aot.hint.annotation.Reflective; + +@Reflective +@SuppressWarnings("unused") +public class Reflective22OnType { +} diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrar.java index 61e80a3fa2e..8489413bfdd 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrar.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import org.springframework.aot.hint.ReflectionHints; @@ -66,6 +67,28 @@ public class ReflectiveRuntimeHintsRegistrar { }); } + /** + * Specify if the given {@code type} is a valid candidate. + * @param type the type to inspect + * @return {@code true} if the type uses {@link Reflective} in a way that + * is supported by this registrar + * @since 6.2 + */ + public boolean isCandidate(Class type) { + if (isReflective(type)) { + return true; + } + AtomicBoolean candidate = new AtomicBoolean(false); + doWithReflectiveConstructors(type, constructor -> candidate.set(true)); + if (!candidate.get()) { + ReflectionUtils.doWithFields(type, field -> candidate.set(true), this::isReflective); + } + if (!candidate.get()) { + ReflectionUtils.doWithMethods(type, method -> candidate.set(true), this::isReflective); + } + return candidate.get(); + } + private void processType(Set entries, Class typeToProcess) { if (isReflective(typeToProcess)) { entries.add(createEntry(typeToProcess)); diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflection.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflection.java index fb72fca8a13..cf904645f85 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflection.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflection.java @@ -36,26 +36,28 @@ import org.springframework.aot.hint.MemberCategory; *

This annotation can be used as a meta-annotation to customize how hints * are registered against each target class. * - *

The annotated element can be any bean: + *

You can use this annotation on any bean that is contributed to the context: *


  * @Configuration
  * @RegisterReflection(classes = CustomerEntry.class, memberCategories = PUBLIC_FIELDS)
- * public class MyConfig {
+ * class MyConfig {
  *     // ...
  * }
* + *

If scanning of {@link Reflective} is enabled, any type in the configured + * packages can use this annotation as well. + * *

To register reflection hints for the type itself, only member categories * should be specified:


  * @Component
  * @RegisterReflection(memberCategories = INVOKE_PUBLIC_METHODS)
- * public class MyComponent {
+ * class MyComponent {
  *     // ...
  * }
* *

Reflection hints can be registered from a method. In this case, at least * one target class should be specified:


- * @Component
- * public class MyComponent {
+ * class MyProcessor {
  *
  *     @RegisterReflection(classes = CustomerEntry.class, memberCategories = PUBLIC_FIELDS)
  *     CustomerEntry process() { ... }
@@ -65,6 +67,9 @@ import org.springframework.aot.hint.MemberCategory;
  * 

If the class is not available, {@link #classNames()} allows to specify the * fully qualified name, rather than the {@link Class} reference. * + *

The annotated element can also be any test class that uses the Spring + * TestContext Framework to load an {@code ApplicationContext}. + * * @author Stephane Nicoll * @since 6.2 */ diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java index c91ab40d4d7..54aaa293329 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java @@ -32,26 +32,29 @@ import org.springframework.core.annotation.AliasFor; * and record components. Hints are also registered for types transitively used * on properties and record components. * - *

The annotated element can be a configuration class — for example: + *

You can use this annotation on any bean that is contributed to the context: *


  * @Configuration
  * @RegisterReflectionForBinding({Foo.class, Bar.class})
- * public class MyConfig {
+ * class MyConfig {
  *     // ...
  * }
* + *

If scanning of {@link Reflective} is enabled, any type in the configured + * packages can use this annotation as well. + * *

When the annotated element is a type, the type itself is registered if no * candidates are provided:


  * @Component
  * @RegisterReflectionForBinding
- * public class MyBean {
+ * class MyBean {
  *     // ...
  * }
* * The annotation can also be specified on a method. In that case, at least one * target class must be specified:

  * @Component
- * public class MyService {
+ * class MyService {
  *
  *     @RegisterReflectionForBinding(Baz.class)
  *     public Baz process() {
diff --git a/spring-core/src/test/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrarTests.java b/spring-core/src/test/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrarTests.java
index e2d860d74a1..5bc8f852fe5 100644
--- a/spring-core/src/test/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrarTests.java
+++ b/spring-core/src/test/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrarTests.java
@@ -16,6 +16,7 @@
 
 package org.springframework.aot.hint.annotation;
 
+import java.io.Closeable;
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -24,6 +25,8 @@ import java.lang.annotation.Target;
 import java.lang.reflect.Method;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import org.springframework.aot.hint.FieldHint;
 import org.springframework.aot.hint.MemberCategory;
@@ -50,6 +53,20 @@ class ReflectiveRuntimeHintsRegistrarTests {
 
 	private final RuntimeHints runtimeHints = new RuntimeHints();
 
+	@ParameterizedTest
+	@ValueSource(classes = { SampleTypeAnnotatedBean.class, SampleFieldAnnotatedBean.class,
+			SampleConstructorAnnotatedBean.class, SampleMethodAnnotatedBean.class, SampleInterface.class,
+			SampleMethodMetaAnnotatedBeanWithAlias.class, SampleMethodAnnotatedBeanWithInterface.class })
+	void isCandidateWithMatchingAnnotatedElement(Class candidate) {
+		assertThat(this.registrar.isCandidate(candidate)).isTrue();
+	}
+
+	@ParameterizedTest
+	@ValueSource(classes = { String.class, Closeable.class })
+	void isCandidateWithNonMatchingAnnotatedElement(Class candidate) {
+		assertThat(this.registrar.isCandidate(candidate)).isFalse();
+	}
+
 	@Test
 	void shouldIgnoreNonAnnotatedType() {
 		RuntimeHints mock = mock();