From 93587da394b9a366826f344516a95b7dc02cefcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 3 May 2024 16:12:39 +0200 Subject: [PATCH] Introduce ReflectiveScan This commit allows `@Reflective` to be used on arbitrary types, not only Spring beans. This makes the feature much more powerful as components can be tagged directly. Scanning happens during AOT processing (typically at build-time) when `@ReflectiveScan` is used. Types do not need to have a particular annotation, and types that can't be loaded are ignored. This commit also exposes the infrastructure that does the scanning so that custom code can do the scanning in an AOT contribution if they don't want to rely on the annotation. Closes gh-33132 --- .../modules/ROOT/pages/core/aot.adoc | 19 ++- .../aot/hints/reflective/MyConfiguration.java | 25 +++ .../annotation/ImportRuntimeHints.java | 6 +- .../context/annotation/ReflectiveScan.java | 90 ++++++++++ ...ectiveProcessorAotContributionBuilder.java | 160 ++++++++++++++++++ ...BeanFactoryInitializationAotProcessor.java | 51 +++--- ...eProcessorAotContributionBuilderTests.java | 114 +++++++++++++ ...actoryInitializationAotProcessorTests.java | 61 +++++++ .../scan/noreflective/ReflectiveNotUsed.java | 21 +++ .../context/aot/scan/reflective/Dummy.java | 21 +++ .../reflective/ReflectiveOnConstructor.java | 35 ++++ .../scan/reflective/ReflectiveOnField.java | 29 ++++ .../reflective/ReflectiveOnInnerField.java | 30 ++++ .../reflective/ReflectiveOnInterface.java | 24 +++ .../scan/reflective/ReflectiveOnMethod.java | 26 +++ .../reflective/ReflectiveOnNestedType.java | 26 +++ .../scan/reflective/ReflectiveOnRecord.java | 24 +++ .../aot/scan/reflective/ReflectiveOnType.java | 24 +++ .../scan/reflective2/Reflective2OnType.java | 24 +++ .../reflective21/Reflective21OnType.java | 24 +++ .../reflective22/Reflective22OnType.java | 24 +++ .../ReflectiveRuntimeHintsRegistrar.java | 25 ++- .../hint/annotation/RegisterReflection.java | 15 +- .../RegisterReflectionForBinding.java | 11 +- .../ReflectiveRuntimeHintsRegistrarTests.java | 17 ++ 25 files changed, 887 insertions(+), 39 deletions(-) create mode 100644 framework-docs/src/main/java/org/springframework/docs/core/aot/hints/reflective/MyConfiguration.java create mode 100644 spring-context/src/main/java/org/springframework/context/annotation/ReflectiveScan.java create mode 100644 spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilder.java create mode 100644 spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilderTests.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/noreflective/ReflectiveNotUsed.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/Dummy.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnConstructor.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnField.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnInnerField.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnInterface.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnMethod.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnNestedType.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnRecord.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective/ReflectiveOnType.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective2/Reflective2OnType.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective2/reflective21/Reflective21OnType.java create mode 100644 spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/aot/scan/reflective2/reflective22/Reflective22OnType.java 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();