Skip to content

Commit 7bc2a7f

Browse files
committed
Add support for programmatic CandidateComponentsIndex setup
Closes gh-35497 See gh-35472
1 parent 97ae5fd commit 7bc2a7f

File tree

9 files changed

+261
-65
lines changed

9 files changed

+261
-65
lines changed

spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,13 @@
3737

3838
/**
3939
* Annotation {@link Processor} that writes a {@link CandidateComponentsMetadata}
40-
* file for spring components.
40+
* file for Spring components.
4141
*
4242
* @author Stephane Nicoll
4343
* @author Juergen Hoeller
4444
* @since 5.0
45-
* @deprecated as of 6.1, in favor of the AOT engine.
45+
* @deprecated as of 6.1, in favor of the AOT engine and the forthcoming
46+
* support for an AOT-generated Spring components index
4647
*/
4748
@Deprecated(since = "6.1", forRemoval = true)
4849
public class CandidateComponentsIndexer implements Processor {

spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@
2525

2626
/**
2727
* A {@link StereotypesProvider} that extracts a stereotype for each
28-
* {@code jakarta.*} or {@code javax.*} annotation <i>present</i> on a class or
29-
* interface.
28+
* {@code jakarta.*} annotation <i>present</i> on a class or interface.
3029
*
3130
* @author Stephane Nicoll
3231
* @since 5.0
@@ -50,7 +49,7 @@ public Set<String> getStereotypes(Element element) {
5049
}
5150
for (AnnotationMirror annotation : this.typeHelper.getAllAnnotationMirrors(element)) {
5251
String type = this.typeHelper.getType(annotation);
53-
if (type.startsWith("jakarta.") || type.startsWith("javax.")) {
52+
if (type.startsWith("jakarta.")) {
5453
stereotypes.add(type);
5554
}
5655
}

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

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,14 @@ public final MetadataReaderFactory getMetadataReaderFactory() {
312312
*/
313313
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
314314
if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
315-
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
316-
}
317-
else {
318-
return scanCandidateComponents(basePackage);
315+
if (this.componentsIndex.hasScannedPackage(basePackage)) {
316+
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
317+
}
318+
else {
319+
this.componentsIndex.registerScan(basePackage);
320+
}
319321
}
322+
return scanCandidateComponents(basePackage);
320323
}
321324

322325
/**
@@ -339,14 +342,13 @@ private boolean indexSupportsIncludeFilters() {
339342
* @param filter the filter to check
340343
* @return whether the index supports this include filter
341344
* @since 5.0
345+
* @see #registerCandidateTypeForIncludeFilter(String, TypeFilter)
342346
* @see #extractStereotype(TypeFilter)
343347
*/
344348
private boolean indexSupportsIncludeFilter(TypeFilter filter) {
345349
if (filter instanceof AnnotationTypeFilter annotationTypeFilter) {
346350
Class<? extends Annotation> annotationType = annotationTypeFilter.getAnnotationType();
347-
return (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, annotationType) ||
348-
annotationType.getName().startsWith("jakarta.") ||
349-
annotationType.getName().startsWith("javax."));
351+
return isStereotypeAnnotationForIndex(annotationType);
350352
}
351353
if (filter instanceof AssignableTypeFilter assignableTypeFilter) {
352354
Class<?> target = assignableTypeFilter.getTargetType();
@@ -355,6 +357,28 @@ private boolean indexSupportsIncludeFilter(TypeFilter filter) {
355357
return false;
356358
}
357359

360+
/**
361+
* Register the given class as a candidate type with the runtime-populated index, if any.
362+
* @param className the fully-qualified class name of the candidate type
363+
* @param filter the include filter to introspect for the associated stereotype
364+
*/
365+
private void registerCandidateTypeForIncludeFilter(String className, TypeFilter filter) {
366+
if (this.componentsIndex != null) {
367+
if (filter instanceof AnnotationTypeFilter annotationTypeFilter) {
368+
Class<? extends Annotation> annotationType = annotationTypeFilter.getAnnotationType();
369+
if (isStereotypeAnnotationForIndex(annotationType)) {
370+
this.componentsIndex.registerCandidateType(className, annotationType.getName());
371+
}
372+
}
373+
else if (filter instanceof AssignableTypeFilter assignableTypeFilter) {
374+
Class<?> target = assignableTypeFilter.getTargetType();
375+
if (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, target)) {
376+
this.componentsIndex.registerCandidateType(className, target.getName());
377+
}
378+
}
379+
}
380+
}
381+
358382
/**
359383
* Extract the stereotype to use for the specified compatible filter.
360384
* @param filter the filter to handle
@@ -372,6 +396,11 @@ private boolean indexSupportsIncludeFilter(TypeFilter filter) {
372396
return null;
373397
}
374398

399+
private boolean isStereotypeAnnotationForIndex(Class<? extends Annotation> annotationType) {
400+
return (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, annotationType) ||
401+
annotationType.getName().startsWith("jakarta."));
402+
}
403+
375404
private Set<BeanDefinition> addCandidateComponentsFromIndex(CandidateComponentsIndex index, String basePackage) {
376405
Set<BeanDefinition> candidates = new LinkedHashSet<>();
377406
try {
@@ -503,13 +532,14 @@ protected String resolveBasePackage(String basePackage) {
503532
* @return whether the class qualifies as a candidate component
504533
*/
505534
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
506-
for (TypeFilter tf : this.excludeFilters) {
507-
if (tf.match(metadataReader, getMetadataReaderFactory())) {
535+
for (TypeFilter filter : this.excludeFilters) {
536+
if (filter.match(metadataReader, getMetadataReaderFactory())) {
508537
return false;
509538
}
510539
}
511-
for (TypeFilter tf : this.includeFilters) {
512-
if (tf.match(metadataReader, getMetadataReaderFactory())) {
540+
for (TypeFilter filter : this.includeFilters) {
541+
if (filter.match(metadataReader, getMetadataReaderFactory())) {
542+
registerCandidateTypeForIncludeFilter(metadataReader.getClassMetadata().getClassName(), filter);
513543
return isConditionMatch(metadataReader);
514544
}
515545
}

spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.context.index;
1818

1919
import java.util.Collections;
20+
import java.util.LinkedHashSet;
2021
import java.util.List;
2122
import java.util.Properties;
2223
import java.util.Set;
@@ -44,35 +45,88 @@
4445
* a target type but it can be any marker really.
4546
*
4647
* @author Stephane Nicoll
48+
* @author Juergen Hoeller
4749
* @since 5.0
48-
* @deprecated as of 6.1, in favor of the AOT engine.
4950
*/
50-
@Deprecated(since = "6.1", forRemoval = true)
5151
public class CandidateComponentsIndex {
5252

5353
private static final AntPathMatcher pathMatcher = new AntPathMatcher(".");
5454

55-
private final MultiValueMap<String, Entry> index;
55+
private final Set<String> registeredScans = new LinkedHashSet<>();
5656

57+
private final MultiValueMap<String, Entry> index = new LinkedMultiValueMap<>();
58+
59+
private final boolean complete;
5760

58-
CandidateComponentsIndex(List<Properties> content) {
59-
this.index = parseIndex(content);
60-
}
6161

62-
private static MultiValueMap<String, Entry> parseIndex(List<Properties> content) {
63-
MultiValueMap<String, Entry> index = new LinkedMultiValueMap<>();
62+
/**
63+
* Create a new index instance from parsed components index files.
64+
*/
65+
CandidateComponentsIndex(List<Properties> content) {
6466
for (Properties entry : content) {
6567
entry.forEach((type, values) -> {
6668
String[] stereotypes = ((String) values).split(",");
6769
for (String stereotype : stereotypes) {
68-
index.add(stereotype, new Entry((String) type));
70+
this.index.add(stereotype, new Entry((String) type));
6971
}
7072
});
7173
}
72-
return index;
74+
this.complete = true;
75+
}
76+
77+
/**
78+
* Create a new index instance for programmatic population.
79+
* @since 7.0
80+
*/
81+
public CandidateComponentsIndex() {
82+
this.complete = false;
7383
}
7484

7585

86+
/**
87+
* Register the given base packages (or base package patterns) as scanned.
88+
* @since 7.0
89+
*/
90+
public void registerScan(String... basePackages) {
91+
Collections.addAll(this.registeredScans, basePackages);
92+
}
93+
94+
/**
95+
* Return the registered base packages (or base package patterns).
96+
* @since 7.0
97+
*/
98+
public Set<String> getRegisteredScans() {
99+
return this.registeredScans;
100+
}
101+
102+
/**
103+
* Determine whether this index contains entries for the given base package
104+
* (or base package pattern).
105+
* @since 7.0
106+
*/
107+
public boolean hasScannedPackage(String packageName) {
108+
return (this.complete ||
109+
this.registeredScans.stream().anyMatch(basePackage -> matchPackage(basePackage, packageName)));
110+
}
111+
112+
/**
113+
* Programmatically register one or more stereotypes for the given candidate type.
114+
* @since 7.0
115+
*/
116+
public void registerCandidateType(String type, String... stereotypes) {
117+
for (String stereotype : stereotypes) {
118+
this.index.add(stereotype, new Entry(type));
119+
}
120+
}
121+
122+
/**
123+
* Return the registered stereotypes packages (or base package patterns).
124+
* @since 7.0
125+
*/
126+
public Set<String> getRegisteredStereotypes() {
127+
return this.index.keySet();
128+
}
129+
76130
/**
77131
* Return the candidate types that are associated with the specified stereotype.
78132
* @param basePackage the package to check for candidates
@@ -83,18 +137,27 @@ private static MultiValueMap<String, Entry> parseIndex(List<Properties> content)
83137
public Set<String> getCandidateTypes(String basePackage, String stereotype) {
84138
List<Entry> candidates = this.index.get(stereotype);
85139
if (candidates != null) {
86-
return candidates.parallelStream()
140+
return candidates.stream()
87141
.filter(t -> t.match(basePackage))
88142
.map(t -> t.type)
89143
.collect(Collectors.toSet());
90144
}
91145
return Collections.emptySet();
92146
}
93147

148+
private static boolean matchPackage(String basePackage, String packageName) {
149+
if (pathMatcher.isPattern(basePackage)) {
150+
return pathMatcher.match(basePackage, packageName);
151+
}
152+
else {
153+
return packageName.startsWith(basePackage);
154+
}
155+
}
156+
94157

95158
private static class Entry {
96159

97-
private final String type;
160+
final String type;
98161

99162
private final String packageName;
100163

@@ -104,12 +167,7 @@ private static class Entry {
104167
}
105168

106169
public boolean match(String basePackage) {
107-
if (pathMatcher.isPattern(basePackage)) {
108-
return pathMatcher.match(basePackage, this.packageName);
109-
}
110-
else {
111-
return this.type.startsWith(basePackage);
112-
}
170+
return matchPackage(basePackage, this.packageName);
113171
}
114172
}
115173

spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,9 @@
3737
* Candidate components index loading mechanism for internal use within the framework.
3838
*
3939
* @author Stephane Nicoll
40+
* @author Juergen Hoeller
4041
* @since 5.0
41-
* @deprecated as of 6.1, in favor of the AOT engine.
4242
*/
43-
@Deprecated(since = "6.1", forRemoval = true)
44-
@SuppressWarnings("removal")
4543
public final class CandidateComponentsIndexLoader {
4644

4745
/**
@@ -119,4 +117,28 @@ private CandidateComponentsIndexLoader() {
119117
}
120118
}
121119

120+
121+
/**
122+
* Programmatically add the given index instance for the given ClassLoader,
123+
* replacing a file-determined index with a programmatically composed index.
124+
* <p>The index instance will usually be pre-populated for AOT runtime setups
125+
* or test scenarios with pre-configured results for runtime-attempted scans.
126+
* Alternatively, it may be empty for it to get populated during AOT processing
127+
* or a test run, for subsequent introspection the index-recorded candidate types.
128+
* @param classLoader the ClassLoader to add the index for
129+
* @param index the associated CandidateComponentsIndex instance
130+
* @since 7.0
131+
*/
132+
public static void addIndex(ClassLoader classLoader, CandidateComponentsIndex index) {
133+
cache.put(classLoader, index);
134+
}
135+
136+
/**
137+
* Clear the runtime index cache.
138+
* @since 7.0
139+
*/
140+
public static void clearCache() {
141+
cache.clear();
142+
}
143+
122144
}

0 commit comments

Comments
 (0)