Skip to content

Commit 93587da

Browse files
committed
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
1 parent f165807 commit 93587da

File tree

25 files changed

+887
-39
lines changed

25 files changed

+887
-39
lines changed

framework-docs/modules/ROOT/pages/core/aot.adoc

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -509,24 +509,33 @@ It is also possible to register an implementation statically by adding an entry
509509
{spring-framework-api}/aot/hint/annotation/Reflective.html[`@Reflective`] provides an idiomatic way to flag the need for reflection on an annotated element.
510510
For instance, `@EventListener` is meta-annotated with `@Reflective` since the underlying implementation invokes the annotated method using reflection.
511511

512-
By default, only Spring beans are considered, and an invocation hint is registered for the annotated element.
513-
This can be tuned by specifying a custom `ReflectiveProcessor` implementation via the
514-
`@Reflective` annotation.
512+
Out-of-the-box, only Spring beans are considered but you can opt-in for scanning using `@ReflectiveScan`.
513+
In the example below, all types of the package `com.example.app` and their subpackages are considered:
514+
515+
include-code::./MyConfiguration[]
516+
517+
Scanning happens during AOT processing and the types in the target packages do not need to have a class-level annotation to be considered.
518+
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.
519+
520+
By default, `@Reflective` registers an invocation hint for the annotated element.
521+
This can be tuned by specifying a custom `ReflectiveProcessor` implementation via the `@Reflective` annotation.
515522

516523
Library authors can reuse this annotation for their own purposes.
517-
If components other than Spring beans need to be processed, a `BeanFactoryInitializationAotProcessor` can detect the relevant types and use `ReflectiveRuntimeHintsRegistrar` to process them.
524+
An example of such customization is covered in the next section.
518525

519526

520527
[[aot.hints.register-reflection]]
521528
=== `@RegisterReflection`
522529

523530
{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.
524531

532+
NOTE: As a specialization of `@Reflective`, this is also detected if you're using `@ReflectiveScan`.
533+
525534
In the following example, public constructors and public methods can be invoked via reflection on `AccountService`:
526535

527536
include-code::./MyConfiguration[tag=snippet,indent=0]
528537

529-
`@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.
538+
`@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.
530539

531540
`@RegisterReflection` can be used as a meta-annotation to provide more specific needs.
532541
{spring-framework-api}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`] is such composed annotation and registers the need for serializing arbitrary types.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.docs.core.aot.hints.reflective;
18+
19+
import org.springframework.context.annotation.Configuration;
20+
import org.springframework.context.annotation.ReflectiveScan;
21+
22+
@Configuration
23+
@ReflectiveScan("com.example.app")
24+
public class MyConfiguration {
25+
}

spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.lang.annotation.RetentionPolicy;
2323
import java.lang.annotation.Target;
2424

25+
import org.springframework.aot.hint.RuntimeHints;
2526
import org.springframework.aot.hint.RuntimeHintsRegistrar;
2627

2728
/**
@@ -61,9 +62,8 @@
6162
* @author Brian Clozel
6263
* @author Stephane Nicoll
6364
* @since 6.0
64-
* @see org.springframework.aot.hint.RuntimeHints
65-
* @see org.springframework.aot.hint.annotation.Reflective
66-
* @see org.springframework.aot.hint.annotation.RegisterReflection
65+
* @see RuntimeHints
66+
* @see ReflectiveScan @ReflectiveScan
6767
*/
6868
@Target({ElementType.TYPE, ElementType.METHOD})
6969
@Retention(RetentionPolicy.RUNTIME)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.context.annotation;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.aot.hint.annotation.Reflective;
26+
import org.springframework.aot.hint.annotation.RegisterReflection;
27+
import org.springframework.core.annotation.AliasFor;
28+
29+
/**
30+
* Scan arbitrary types for use of {@link Reflective}. Typically used on
31+
* {@link Configuration @Configuration} classes but can be added to any bean.
32+
* Scanning happens during AOT processing, typically at build-time.
33+
*
34+
* <p>In the example below, {@code com.example.app} and its subpackages are
35+
* scanned: <pre><code class="java">
36+
* &#064;Configuration
37+
* &#064;ReflectiveScan("com.example.app")
38+
* class MyConfiguration {
39+
* // ...
40+
* }</code></pre>
41+
*
42+
* <p>Either {@link #basePackageClasses} or {@link #basePackages} (or its alias
43+
* {@link #value}) may be specified to define specific packages to scan. If specific
44+
* packages are not defined, scanning will occur recursively beginning with the
45+
* package of the class that declares this annotation.
46+
*
47+
* <p>A type does not need to be annotated at class level to be candidate, and
48+
* this performs a "deep scan" by loading every class in the target packages and
49+
* search for {@link Reflective} on types, constructors, methods, and fields.
50+
* Enclosed classes are candidates as well. Classes that fail to load are
51+
* ignored.
52+
*
53+
* @author Stephane Nicoll
54+
* @see Reflective @Reflective
55+
* @see RegisterReflection @RegisterReflection
56+
* @since 6.2
57+
*/
58+
@Retention(RetentionPolicy.RUNTIME)
59+
@Target(ElementType.TYPE)
60+
@Documented
61+
public @interface ReflectiveScan {
62+
63+
/**
64+
* Alias for {@link #basePackages}.
65+
* <p>Allows for more concise annotation declarations if no other attributes
66+
* are needed &mdash; for example, {@code @ReflectiveScan("org.my.pkg")}
67+
* instead of {@code @ReflectiveScan(basePackages = "org.my.pkg")}.
68+
*/
69+
@AliasFor("basePackages")
70+
String[] value() default {};
71+
72+
/**
73+
* Base packages to scan for reflective usage.
74+
* <p>{@link #value} is an alias for (and mutually exclusive with) this
75+
* attribute.
76+
* <p>Use {@link #basePackageClasses} for a type-safe alternative to
77+
* String-based package names.
78+
*/
79+
@AliasFor("value")
80+
String[] basePackages() default {};
81+
82+
/**
83+
* Type-safe alternative to {@link #basePackages} for specifying the packages
84+
* to scan for reflection usage. The package of each class specified will be scanned.
85+
* <p>Consider creating a special no-op marker class or interface in each package
86+
* that serves no purpose other than being referenced by this attribute.
87+
*/
88+
Class<?>[] basePackageClasses() default {};
89+
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.context.aot;
18+
19+
import java.util.Arrays;
20+
import java.util.HashSet;
21+
import java.util.LinkedHashSet;
22+
import java.util.Set;
23+
import java.util.stream.StreamSupport;
24+
25+
import org.springframework.aot.generate.GenerationContext;
26+
import org.springframework.aot.hint.RuntimeHints;
27+
import org.springframework.aot.hint.annotation.Reflective;
28+
import org.springframework.aot.hint.annotation.ReflectiveProcessor;
29+
import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar;
30+
import org.springframework.aot.hint.annotation.RegisterReflection;
31+
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
32+
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
33+
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
34+
import org.springframework.beans.factory.config.BeanDefinition;
35+
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
36+
import org.springframework.lang.Nullable;
37+
import org.springframework.util.ClassUtils;
38+
39+
/**
40+
* Builder for an {@linkplain BeanFactoryInitializationAotContribution AOT
41+
* contribution} that detects the presence of {@link Reflective @Reflective} on
42+
* annotated elements and invoke the underlying {@link ReflectiveProcessor}
43+
* implementations.
44+
*
45+
* <p>Candidates can be provided explicitly or by scanning the classpath.
46+
*
47+
* @author Stephane Nicoll
48+
* @since 6.2
49+
* @see Reflective
50+
* @see RegisterReflection
51+
*/
52+
public class ReflectiveProcessorAotContributionBuilder {
53+
54+
private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar();
55+
56+
private final Set<Class<?>> classes = new LinkedHashSet<>();
57+
58+
59+
/**
60+
* Process the given classes by checking the ones that use {@link Reflective}.
61+
* <p>A class is candidate if it uses {@link Reflective} directly or via a
62+
* meta-annotation. Type, fields, constructors, methods and enclosed types
63+
* are inspected.
64+
* @param classes the classes to inspect
65+
*/
66+
public ReflectiveProcessorAotContributionBuilder withClasses(Iterable<Class<?>> classes) {
67+
this.classes.addAll(StreamSupport.stream(classes.spliterator(), false)
68+
.filter(registrar::isCandidate).toList());
69+
return this;
70+
}
71+
72+
/**
73+
* Process the given classes by checking the ones that use {@link Reflective}.
74+
* <p>A class is candidate if it uses {@link Reflective} directly or via a
75+
* meta-annotation. Type, fields, constructors, methods and enclosed types
76+
* are inspected.
77+
* @param classes the classes to inspect
78+
*/
79+
public ReflectiveProcessorAotContributionBuilder withClasses(Class<?>[] classes) {
80+
return withClasses(Arrays.asList(classes));
81+
}
82+
83+
/**
84+
* Scan the given {@code packageNames} and their sub-packages for classes
85+
* that uses {@link Reflective}.
86+
* <p>This performs a "deep scan" by loading every class in the specified
87+
* packages and search for {@link Reflective} on types, constructors, methods,
88+
* and fields. Enclosed classes are candidates as well. Classes that fail to
89+
* load are ignored.
90+
* @param classLoader the classloader to use
91+
* @param packageNames the package names to scan
92+
*/
93+
public ReflectiveProcessorAotContributionBuilder scan(@Nullable ClassLoader classLoader, String... packageNames) {
94+
ReflectiveClassPathScanner scanner = new ReflectiveClassPathScanner(classLoader);
95+
return withClasses(scanner.scan(packageNames));
96+
}
97+
98+
@Nullable
99+
public BeanFactoryInitializationAotContribution build() {
100+
return (!this.classes.isEmpty() ? new AotContribution(this.classes) : null);
101+
}
102+
103+
private static class AotContribution implements BeanFactoryInitializationAotContribution {
104+
105+
private final Class<?>[] classes;
106+
107+
public AotContribution(Set<Class<?>> classes) {
108+
this.classes = classes.toArray(Class<?>[]::new);
109+
}
110+
111+
@Override
112+
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
113+
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
114+
registrar.registerRuntimeHints(runtimeHints, this.classes);
115+
}
116+
117+
}
118+
119+
private static class ReflectiveClassPathScanner extends ClassPathScanningCandidateComponentProvider {
120+
121+
@Nullable
122+
private final ClassLoader classLoader;
123+
124+
ReflectiveClassPathScanner(@Nullable ClassLoader classLoader) {
125+
super(false);
126+
this.classLoader = classLoader;
127+
addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
128+
}
129+
130+
Class<?>[] scan(String... packageNames) {
131+
if (logger.isDebugEnabled()) {
132+
logger.debug("Scanning all types for reflective usage from " + Arrays.toString(packageNames));
133+
}
134+
Set<BeanDefinition> candidates = new HashSet<>();
135+
for (String packageName : packageNames) {
136+
candidates.addAll(findCandidateComponents(packageName));
137+
}
138+
return candidates.stream().map(c -> (Class<?>) c.getAttribute("type")).toArray(Class<?>[]::new);
139+
}
140+
141+
@Override
142+
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
143+
String className = beanDefinition.getBeanClassName();
144+
if (className != null) {
145+
try {
146+
Class<?> type = ClassUtils.forName(className, this.classLoader);
147+
beanDefinition.setAttribute("type", type);
148+
return registrar.isCandidate(type);
149+
}
150+
catch (Exception ex) {
151+
if (logger.isTraceEnabled()) {
152+
logger.trace("Ignoring '%s' for reflective usage: %s".formatted(className, ex.getMessage()));
153+
}
154+
}
155+
}
156+
return false;
157+
}
158+
}
159+
160+
}

0 commit comments

Comments
 (0)