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();