Skip to content

Commit 4cad077

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 spring-projectsgh-33132
1 parent f165807 commit 4cad077

File tree

21 files changed

+799
-25
lines changed

21 files changed

+799
-25
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotContribution.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.beans.factory.aot;
1818

1919
import org.springframework.aot.generate.GenerationContext;
20+
import org.springframework.lang.Nullable;
2021

2122
/**
2223
* AOT contribution from a {@link BeanFactoryInitializationAotProcessor} used to
@@ -27,6 +28,7 @@
2728
* {@link org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter}.
2829
*
2930
* @author Phillip Webb
31+
* @author Stephane Nicoll
3032
* @since 6.0
3133
* @see BeanFactoryInitializationAotProcessor
3234
*/
@@ -41,4 +43,31 @@ public interface BeanFactoryInitializationAotContribution {
4143
void applyTo(GenerationContext generationContext,
4244
BeanFactoryInitializationCode beanFactoryInitializationCode);
4345

46+
/**
47+
* Create a contribution that applies the contribution of the first contribution
48+
* followed by the second contribution. Any contribution can be {@code null} to be
49+
* ignored and the concatenated contribution is {@code null} if both inputs are
50+
* {@code null}.
51+
* @param a the first contribution
52+
* @param b the second contribution
53+
* @return the concatenation of the two contributions, or {@code null} if
54+
* they are both {@code null}.
55+
* @since 6.2
56+
*/
57+
@Nullable
58+
static BeanFactoryInitializationAotContribution concat(@Nullable BeanFactoryInitializationAotContribution a,
59+
@Nullable BeanFactoryInitializationAotContribution b) {
60+
61+
if (a == null) {
62+
return b;
63+
}
64+
if (b == null) {
65+
return a;
66+
}
67+
return (generationContext, code) -> {
68+
a.applyTo(generationContext, code);
69+
b.applyTo(generationContext, code);
70+
};
71+
}
72+
4473
}

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
6767
*/
6868
@Target({ElementType.TYPE, ElementType.METHOD})
6969
@Retention(RetentionPolicy.RUNTIME)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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.core.annotation.AliasFor;
27+
28+
/**
29+
* Scan arbitrary types for use of {@link Reflective}. Typically used on
30+
* {@link Configuration @Configuration} classes but can be added to any bean.
31+
*
32+
* <p>In the example below, {@code com.example.app} and its subpackages are
33+
* scanned: <pre><code class="java">
34+
* &#064;Configuration
35+
* &#064;ReflectiveScan("com.example.app")
36+
* class MyConfiguration {
37+
* // ...
38+
* }</code></pre>
39+
*
40+
* <p>Either {@link #basePackageClasses} or {@link #basePackages} (or its alias
41+
* {@link #value}) may be specified to define specific packages to scan. If specific
42+
* packages are not defined, scanning will occur recursively beginning with the
43+
* package of the class that declares this annotation.
44+
*
45+
* <p>A type does not need to be annotated at class level to be candidate, and
46+
* this performs a "deep scan" by loading every class in the target packages and
47+
* search for {@link Reflective} on types, constructors, methods, and fields.
48+
* Enclosed classes are candidates as well. Classes that fail to load are
49+
* ignored.
50+
*
51+
* <p>Scanning happens during AOT processing, typically at build-time.
52+
*
53+
* @author Stephane Nicoll
54+
* @see Reflective
55+
* @since 6.2
56+
*/
57+
@Retention(RetentionPolicy.RUNTIME)
58+
@Target(ElementType.TYPE)
59+
@Documented
60+
public @interface ReflectiveScan {
61+
62+
/**
63+
* Alias for {@link #basePackages}.
64+
* <p>Allows for more concise annotation declarations if no other attributes
65+
* are needed &mdash; for example, {@code @ReflectiveScan("org.my.pkg")}
66+
* instead of {@code @ReflectiveScan(basePackages = "org.my.pkg")}.
67+
*/
68+
@AliasFor("basePackages")
69+
String[] value() default {};
70+
71+
/**
72+
* Base packages to scan for reflective usage.
73+
* <p>{@link #value} is an alias for (and mutually exclusive with) this
74+
* attribute.
75+
* <p>Use {@link #basePackageClasses} for a type-safe alternative to
76+
* String-based package names.
77+
*/
78+
@AliasFor("value")
79+
String[] basePackages() default {};
80+
81+
/**
82+
* Type-safe alternative to {@link #basePackages} for specifying the packages
83+
* to scan for reflection usage. The package of each class specified will be scanned.
84+
* <p>Consider creating a special no-op marker class or interface in each package
85+
* that serves no purpose other than being referenced by this attribute.
86+
*/
87+
Class<?>[] basePackageClasses() default {};
88+
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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.Set;
22+
import java.util.stream.StreamSupport;
23+
24+
import org.springframework.aot.generate.GenerationContext;
25+
import org.springframework.aot.hint.RuntimeHints;
26+
import org.springframework.aot.hint.annotation.Reflective;
27+
import org.springframework.aot.hint.annotation.ReflectiveProcessor;
28+
import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar;
29+
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
30+
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
31+
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
32+
import org.springframework.beans.factory.config.BeanDefinition;
33+
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
34+
import org.springframework.lang.Nullable;
35+
import org.springframework.util.ClassUtils;
36+
37+
/**
38+
* Helper class to create an AOT contribution that detects the presence of
39+
* {@link Reflective @Reflective} on annotated elements and invoke the underlying
40+
* {@link ReflectiveProcessor} implementations.
41+
*
42+
* @author Stephane Nicoll
43+
* @since 6.2
44+
*/
45+
public abstract class ReflectiveProcessorAotContributionProvider {
46+
47+
private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar();
48+
49+
/**
50+
* Create an AOT contribution from the given classes by checking the ones
51+
* that use {@link Reflective}. The returned contribution registers the
52+
* necessary reflection hints as a result. If no class amongst the given
53+
* classes use {@link Reflective}, or if the given iterable is empty,
54+
* returns {@code null}.
55+
* @param classes the classes to inspect
56+
* @return an AOT contribution for the classes that use {@link Reflective}
57+
* or {@code null} if they aren't any
58+
*/
59+
@Nullable
60+
public static BeanFactoryInitializationAotContribution from(Iterable<Class<?>> classes) {
61+
return from(StreamSupport.stream(classes.spliterator(), false).toArray(Class<?>[]::new));
62+
}
63+
64+
/**
65+
* Create an AOT contribution from the given classes by checking the ones
66+
* that use {@link Reflective}. The returned contribution registers the
67+
* necessary reflection hints as a result. If no class amongst the given
68+
* classes use {@link Reflective}, or if the given iterable is empty,
69+
* returns {@code null}.
70+
* @param classes the classes to inspect
71+
* @return an AOT contribution for the classes that use {@link Reflective}
72+
* or {@code null} if they aren't any
73+
*/
74+
@Nullable
75+
public static BeanFactoryInitializationAotContribution from(Class<?>[] classes) {
76+
Class<?>[] types = Arrays.stream(classes).filter(registrar::isCandidate).toArray(Class<?>[]::new);
77+
return (types.length > 0 ? new AotContribution(types) : null);
78+
}
79+
80+
/**
81+
* Scan the given {@code packageNames} and their sub-packages for classes
82+
* that uses {@link Reflective} and create an AOT contribution with the
83+
* result. If no candidates were found, return {@code null}.
84+
* <p>This performs a "deep scan" by loading every class in the specified
85+
* packages and search for {@link Reflective} on types, constructors, methods,
86+
* and fields. Enclosed classes are candidates as well. Classes that fail to
87+
* load are ignored.
88+
* @param classLoader the classloader to use
89+
* @param packageNames the package names to scan
90+
* @return an AOT contribution for the identified classes or {@code null} if
91+
* they aren't any
92+
*/
93+
@Nullable
94+
public static BeanFactoryInitializationAotContribution scan(@Nullable ClassLoader classLoader, String... packageNames) {
95+
ReflectiveClassPathScanner scanner = new ReflectiveClassPathScanner(classLoader);
96+
Class<?>[] types = scanner.scan(packageNames);
97+
return (types.length > 0 ? new AotContribution(types) : null);
98+
}
99+
100+
private static class AotContribution implements BeanFactoryInitializationAotContribution {
101+
102+
private final Class<?>[] classes;
103+
104+
public AotContribution(Class<?>[] classes) {
105+
this.classes = classes;
106+
}
107+
108+
@Override
109+
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
110+
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
111+
registrar.registerRuntimeHints(runtimeHints, this.classes);
112+
}
113+
114+
}
115+
116+
private static class ReflectiveClassPathScanner extends ClassPathScanningCandidateComponentProvider {
117+
118+
@Nullable
119+
private final ClassLoader classLoader;
120+
121+
ReflectiveClassPathScanner(@Nullable ClassLoader classLoader) {
122+
super(false);
123+
this.classLoader = classLoader;
124+
addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
125+
}
126+
127+
Class<?>[] scan(String... packageNames) {
128+
if (logger.isDebugEnabled()) {
129+
logger.debug("Scanning all types for reflective usage from " + Arrays.toString(packageNames));
130+
}
131+
Set<BeanDefinition> candidates = new HashSet<>();
132+
for (String packageName : packageNames) {
133+
candidates.addAll(findCandidateComponents(packageName));
134+
}
135+
return candidates.stream().map(c -> (Class<?>) c.getAttribute("type")).toArray(Class<?>[]::new);
136+
}
137+
138+
@Override
139+
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
140+
String className = beanDefinition.getBeanClassName();
141+
if (className != null) {
142+
try {
143+
Class<?> type = ClassUtils.forName(className, this.classLoader);
144+
beanDefinition.setAttribute("type", type);
145+
return registrar.isCandidate(type);
146+
}
147+
catch (Exception ex) {
148+
if (logger.isTraceEnabled()) {
149+
logger.trace("Ignoring '%s' for reflective usage: %s".formatted(className, ex.getMessage()));
150+
}
151+
}
152+
}
153+
return false;
154+
}
155+
}
156+
157+
}

spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,20 @@
1717
package org.springframework.context.aot;
1818

1919
import java.util.Arrays;
20+
import java.util.Collections;
21+
import java.util.LinkedHashSet;
22+
import java.util.Set;
2023

21-
import org.springframework.aot.generate.GenerationContext;
22-
import org.springframework.aot.hint.RuntimeHints;
2324
import org.springframework.aot.hint.annotation.Reflective;
2425
import org.springframework.aot.hint.annotation.ReflectiveProcessor;
25-
import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar;
2626
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
2727
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
28-
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
2928
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
3029
import org.springframework.beans.factory.support.RegisteredBean;
30+
import org.springframework.context.annotation.ReflectiveScan;
31+
import org.springframework.core.annotation.AnnotatedElementUtils;
32+
import org.springframework.lang.Nullable;
33+
import org.springframework.util.ClassUtils;
3134

3235
/**
3336
* AOT {@code BeanFactoryInitializationAotProcessor} that detects the presence
@@ -39,32 +42,39 @@
3942
*/
4043
class ReflectiveProcessorBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor {
4144

42-
private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar();
43-
44-
4545
@Override
46+
@Nullable
4647
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
47-
Class<?>[] beanTypes = Arrays.stream(beanFactory.getBeanDefinitionNames())
48+
Class<?>[] beanClasses = Arrays.stream(beanFactory.getBeanDefinitionNames())
4849
.map(beanName -> RegisteredBean.of(beanFactory, beanName).getBeanClass())
4950
.toArray(Class<?>[]::new);
50-
return new ReflectiveProcessorBeanFactoryInitializationAotContribution(beanTypes);
51+
String[] packagesToScan = findBasePackagesToScan(beanClasses);
52+
return BeanFactoryInitializationAotContribution.concat(
53+
ReflectiveProcessorAotContributionProvider.scan(beanFactory.getBeanClassLoader(), packagesToScan),
54+
ReflectiveProcessorAotContributionProvider.from(beanClasses));
5155
}
5256

53-
54-
private static class ReflectiveProcessorBeanFactoryInitializationAotContribution
55-
implements BeanFactoryInitializationAotContribution {
56-
57-
private final Class<?>[] types;
58-
59-
public ReflectiveProcessorBeanFactoryInitializationAotContribution(Class<?>[] types) {
60-
this.types = types;
57+
protected String[] findBasePackagesToScan(Class<?>[] beanClasses) {
58+
Set<String> basePackages = new LinkedHashSet<>();
59+
for (Class<?> beanClass : beanClasses) {
60+
ReflectiveScan reflectiveScan = AnnotatedElementUtils.getMergedAnnotation(beanClass, ReflectiveScan.class);
61+
if (reflectiveScan != null) {
62+
basePackages.addAll(extractBasePackages(reflectiveScan, beanClass));
63+
}
6164
}
65+
return basePackages.toArray(new String[0]);
66+
}
6267

63-
@Override
64-
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
65-
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
66-
registrar.registerRuntimeHints(runtimeHints, this.types);
68+
private Set<String> extractBasePackages(ReflectiveScan annotation, Class<?> declaringClass) {
69+
Set<String> basePackages = new LinkedHashSet<>();
70+
Collections.addAll(basePackages, annotation.basePackages());
71+
for (Class<?> clazz : annotation.basePackageClasses()) {
72+
basePackages.add(ClassUtils.getPackageName(clazz));
73+
}
74+
if (basePackages.isEmpty()) {
75+
basePackages.add(ClassUtils.getPackageName(declaringClass));
6776
}
77+
return basePackages;
6878
}
6979

7080
}

0 commit comments

Comments
 (0)