Add support for programmatic CandidateComponentsIndex setup

Closes gh-35497
See gh-35472
This commit is contained in:
Juergen Hoeller
2025-10-09 20:11:11 +02:00
parent 97ae5fde7c
commit 7bc2a7f3f2
9 changed files with 267 additions and 71 deletions
@@ -312,11 +312,14 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC
*/
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
}
else {
return scanCandidateComponents(basePackage);
if (this.componentsIndex.hasScannedPackage(basePackage)) {
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
}
else {
this.componentsIndex.registerScan(basePackage);
}
}
return scanCandidateComponents(basePackage);
}
/**
@@ -339,14 +342,13 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC
* @param filter the filter to check
* @return whether the index supports this include filter
* @since 5.0
* @see #registerCandidateTypeForIncludeFilter(String, TypeFilter)
* @see #extractStereotype(TypeFilter)
*/
private boolean indexSupportsIncludeFilter(TypeFilter filter) {
if (filter instanceof AnnotationTypeFilter annotationTypeFilter) {
Class<? extends Annotation> annotationType = annotationTypeFilter.getAnnotationType();
return (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, annotationType) ||
annotationType.getName().startsWith("jakarta.") ||
annotationType.getName().startsWith("javax."));
return isStereotypeAnnotationForIndex(annotationType);
}
if (filter instanceof AssignableTypeFilter assignableTypeFilter) {
Class<?> target = assignableTypeFilter.getTargetType();
@@ -355,6 +357,28 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC
return false;
}
/**
* Register the given class as a candidate type with the runtime-populated index, if any.
* @param className the fully-qualified class name of the candidate type
* @param filter the include filter to introspect for the associated stereotype
*/
private void registerCandidateTypeForIncludeFilter(String className, TypeFilter filter) {
if (this.componentsIndex != null) {
if (filter instanceof AnnotationTypeFilter annotationTypeFilter) {
Class<? extends Annotation> annotationType = annotationTypeFilter.getAnnotationType();
if (isStereotypeAnnotationForIndex(annotationType)) {
this.componentsIndex.registerCandidateType(className, annotationType.getName());
}
}
else if (filter instanceof AssignableTypeFilter assignableTypeFilter) {
Class<?> target = assignableTypeFilter.getTargetType();
if (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, target)) {
this.componentsIndex.registerCandidateType(className, target.getName());
}
}
}
}
/**
* Extract the stereotype to use for the specified compatible filter.
* @param filter the filter to handle
@@ -372,6 +396,11 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC
return null;
}
private boolean isStereotypeAnnotationForIndex(Class<? extends Annotation> annotationType) {
return (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, annotationType) ||
annotationType.getName().startsWith("jakarta."));
}
private Set<BeanDefinition> addCandidateComponentsFromIndex(CandidateComponentsIndex index, String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
@@ -503,13 +532,14 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC
* @return whether the class qualifies as a candidate component
*/
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
for (TypeFilter filter : this.excludeFilters) {
if (filter.match(metadataReader, getMetadataReaderFactory())) {
return false;
}
}
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
for (TypeFilter filter : this.includeFilters) {
if (filter.match(metadataReader, getMetadataReaderFactory())) {
registerCandidateTypeForIncludeFilter(metadataReader.getClassMetadata().getClassName(), filter);
return isConditionMatch(metadataReader);
}
}
@@ -17,6 +17,7 @@
package org.springframework.context.index;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
@@ -44,34 +45,87 @@ import org.springframework.util.MultiValueMap;
* a target type but it can be any marker really.
*
* @author Stephane Nicoll
* @author Juergen Hoeller
* @since 5.0
* @deprecated as of 6.1, in favor of the AOT engine.
*/
@Deprecated(since = "6.1", forRemoval = true)
public class CandidateComponentsIndex {
private static final AntPathMatcher pathMatcher = new AntPathMatcher(".");
private final MultiValueMap<String, Entry> index;
private final Set<String> registeredScans = new LinkedHashSet<>();
private final MultiValueMap<String, Entry> index = new LinkedMultiValueMap<>();
private final boolean complete;
/**
* Create a new index instance from parsed components index files.
*/
CandidateComponentsIndex(List<Properties> content) {
this.index = parseIndex(content);
}
private static MultiValueMap<String, Entry> parseIndex(List<Properties> content) {
MultiValueMap<String, Entry> index = new LinkedMultiValueMap<>();
for (Properties entry : content) {
entry.forEach((type, values) -> {
String[] stereotypes = ((String) values).split(",");
for (String stereotype : stereotypes) {
index.add(stereotype, new Entry((String) type));
this.index.add(stereotype, new Entry((String) type));
}
});
}
return index;
this.complete = true;
}
/**
* Create a new index instance for programmatic population.
* @since 7.0
*/
public CandidateComponentsIndex() {
this.complete = false;
}
/**
* Register the given base packages (or base package patterns) as scanned.
* @since 7.0
*/
public void registerScan(String... basePackages) {
Collections.addAll(this.registeredScans, basePackages);
}
/**
* Return the registered base packages (or base package patterns).
* @since 7.0
*/
public Set<String> getRegisteredScans() {
return this.registeredScans;
}
/**
* Determine whether this index contains entries for the given base package
* (or base package pattern).
* @since 7.0
*/
public boolean hasScannedPackage(String packageName) {
return (this.complete ||
this.registeredScans.stream().anyMatch(basePackage -> matchPackage(basePackage, packageName)));
}
/**
* Programmatically register one or more stereotypes for the given candidate type.
* @since 7.0
*/
public void registerCandidateType(String type, String... stereotypes) {
for (String stereotype : stereotypes) {
this.index.add(stereotype, new Entry(type));
}
}
/**
* Return the registered stereotypes packages (or base package patterns).
* @since 7.0
*/
public Set<String> getRegisteredStereotypes() {
return this.index.keySet();
}
/**
* Return the candidate types that are associated with the specified stereotype.
@@ -83,7 +137,7 @@ public class CandidateComponentsIndex {
public Set<String> getCandidateTypes(String basePackage, String stereotype) {
List<Entry> candidates = this.index.get(stereotype);
if (candidates != null) {
return candidates.parallelStream()
return candidates.stream()
.filter(t -> t.match(basePackage))
.map(t -> t.type)
.collect(Collectors.toSet());
@@ -91,10 +145,19 @@ public class CandidateComponentsIndex {
return Collections.emptySet();
}
private static boolean matchPackage(String basePackage, String packageName) {
if (pathMatcher.isPattern(basePackage)) {
return pathMatcher.match(basePackage, packageName);
}
else {
return packageName.startsWith(basePackage);
}
}
private static class Entry {
private final String type;
final String type;
private final String packageName;
@@ -104,12 +167,7 @@ public class CandidateComponentsIndex {
}
public boolean match(String basePackage) {
if (pathMatcher.isPattern(basePackage)) {
return pathMatcher.match(basePackage, this.packageName);
}
else {
return this.type.startsWith(basePackage);
}
return matchPackage(basePackage, this.packageName);
}
}
@@ -37,11 +37,9 @@ import org.springframework.util.ConcurrentReferenceHashMap;
* Candidate components index loading mechanism for internal use within the framework.
*
* @author Stephane Nicoll
* @author Juergen Hoeller
* @since 5.0
* @deprecated as of 6.1, in favor of the AOT engine.
*/
@Deprecated(since = "6.1", forRemoval = true)
@SuppressWarnings("removal")
public final class CandidateComponentsIndexLoader {
/**
@@ -119,4 +117,28 @@ public final class CandidateComponentsIndexLoader {
}
}
/**
* Programmatically add the given index instance for the given ClassLoader,
* replacing a file-determined index with a programmatically composed index.
* <p>The index instance will usually be pre-populated for AOT runtime setups
* or test scenarios with pre-configured results for runtime-attempted scans.
* Alternatively, it may be empty for it to get populated during AOT processing
* or a test run, for subsequent introspection the index-recorded candidate types.
* @param classLoader the ClassLoader to add the index for
* @param index the associated CandidateComponentsIndex instance
* @since 7.0
*/
public static void addIndex(ClassLoader classLoader, CandidateComponentsIndex index) {
cache.put(classLoader, index);
}
/**
* Clear the runtime index cache.
* @since 7.0
*/
public static void clearCache() {
cache.clear();
}
}