All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider Maven / Gradle / Ivy

There is a newer version: 6.2.0
Show newest version
/*
 * Copyright 2002-2023 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.io.FileNotFoundException;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.index.CandidateComponentsIndex;
import org.springframework.context.index.CandidateComponentsIndexLoader;
import org.springframework.core.SpringProperties;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.Environment;
import org.springframework.core.env.EnvironmentCapable;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.ClassFormatException;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Indexed;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

/**
 * A component provider that scans for candidate components starting from a
 * specified base package. Can use the {@linkplain CandidateComponentsIndex component
 * index}, if it is available, and scans the classpath otherwise.
 *
 * 

Candidate components are identified by applying exclude and include filters. * {@link AnnotationTypeFilter} and {@link AssignableTypeFilter} include filters * for an annotation/target-type that is annotated with {@link Indexed} are * supported: if any other include filter is specified, the index is ignored and * classpath scanning is used instead. * *

This implementation is based on Spring's * {@link org.springframework.core.type.classreading.MetadataReader MetadataReader} * facility, backed by an ASM {@link org.springframework.asm.ClassReader ClassReader}. * * @author Mark Fisher * @author Juergen Hoeller * @author Ramnivas Laddad * @author Chris Beams * @author Stephane Nicoll * @author Sam Brannen * @since 2.5 * @see org.springframework.core.type.classreading.MetadataReaderFactory * @see org.springframework.core.type.AnnotationMetadata * @see ScannedGenericBeanDefinition * @see CandidateComponentsIndex */ @SuppressWarnings("removal") // components index public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware { static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; /** * System property that instructs Spring to ignore class format exceptions during * classpath scanning, in particular for unsupported class file versions. * By default, such a class format mismatch leads to a classpath scanning failure. * @since 6.1.2 * @see ClassFormatException */ public static final String IGNORE_CLASSFORMAT_PROPERTY_NAME = "spring.classformat.ignore"; private static final boolean shouldIgnoreClassFormatException = SpringProperties.getFlag(IGNORE_CLASSFORMAT_PROPERTY_NAME); protected final Log logger = LogFactory.getLog(getClass()); private String resourcePattern = DEFAULT_RESOURCE_PATTERN; private final List includeFilters = new ArrayList<>(); private final List excludeFilters = new ArrayList<>(); @Nullable private Environment environment; @Nullable private ConditionEvaluator conditionEvaluator; @Nullable private ResourcePatternResolver resourcePatternResolver; @Nullable private MetadataReaderFactory metadataReaderFactory; @Nullable private CandidateComponentsIndex componentsIndex; /** * Protected constructor for flexible subclass initialization. * @since 4.3.6 */ protected ClassPathScanningCandidateComponentProvider() { } /** * Create a ClassPathScanningCandidateComponentProvider with a {@link StandardEnvironment}. * @param useDefaultFilters whether to register the default filters for the * {@link Component @Component}, {@link Repository @Repository}, * {@link Service @Service}, and {@link Controller @Controller} * stereotype annotations * @see #registerDefaultFilters() */ public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters) { this(useDefaultFilters, new StandardEnvironment()); } /** * Create a ClassPathScanningCandidateComponentProvider with the given {@link Environment}. * @param useDefaultFilters whether to register the default filters for the * {@link Component @Component}, {@link Repository @Repository}, * {@link Service @Service}, and {@link Controller @Controller} * stereotype annotations * @param environment the Environment to use * @see #registerDefaultFilters() */ public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters, Environment environment) { if (useDefaultFilters) { registerDefaultFilters(); } setEnvironment(environment); setResourceLoader(null); } /** * Set the resource pattern to use when scanning the classpath. * This value will be appended to each base package name. * @see #findCandidateComponents(String) * @see #DEFAULT_RESOURCE_PATTERN */ public void setResourcePattern(String resourcePattern) { Assert.notNull(resourcePattern, "'resourcePattern' must not be null"); this.resourcePattern = resourcePattern; } /** * Add an include type filter to the end of the inclusion list. */ public void addIncludeFilter(TypeFilter includeFilter) { this.includeFilters.add(includeFilter); } /** * Add an exclude type filter to the front of the exclusion list. */ public void addExcludeFilter(TypeFilter excludeFilter) { this.excludeFilters.add(0, excludeFilter); } /** * Reset the configured type filters. * @param useDefaultFilters whether to re-register the default filters for * the {@link Component @Component}, {@link Repository @Repository}, * {@link Service @Service}, and {@link Controller @Controller} * stereotype annotations * @see #registerDefaultFilters() */ public void resetFilters(boolean useDefaultFilters) { this.includeFilters.clear(); this.excludeFilters.clear(); if (useDefaultFilters) { registerDefaultFilters(); } } /** * Register the default filter for {@link Component @Component}. *

This will implicitly register all annotations that have the * {@link Component @Component} meta-annotation including the * {@link Repository @Repository}, {@link Service @Service}, and * {@link Controller @Controller} stereotype annotations. *

Also supports Jakarta EE's {@link jakarta.annotation.ManagedBean} and * JSR-330's {@link jakarta.inject.Named} annotations (as well as their * pre-Jakarta {@code javax.annotation.ManagedBean} and {@code javax.inject.Named} * equivalents), if available. */ @SuppressWarnings("unchecked") protected void registerDefaultFilters() { this.includeFilters.add(new AnnotationTypeFilter(Component.class)); ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader(); try { this.includeFilters.add(new AnnotationTypeFilter( ((Class) ClassUtils.forName("jakarta.annotation.ManagedBean", cl)), false)); logger.trace("JSR-250 'jakarta.annotation.ManagedBean' found and supported for component scanning"); } catch (ClassNotFoundException ex) { // JSR-250 1.1 API (as included in Jakarta EE) not available - simply skip. } try { this.includeFilters.add(new AnnotationTypeFilter( ((Class) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false)); logger.trace("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning"); } catch (ClassNotFoundException ex) { // JSR-250 1.1 API not available - simply skip. } try { this.includeFilters.add(new AnnotationTypeFilter( ((Class) ClassUtils.forName("jakarta.inject.Named", cl)), false)); logger.trace("JSR-330 'jakarta.inject.Named' annotation found and supported for component scanning"); } catch (ClassNotFoundException ex) { // JSR-330 API (as included in Jakarta EE) not available - simply skip. } try { this.includeFilters.add(new AnnotationTypeFilter( ((Class) ClassUtils.forName("javax.inject.Named", cl)), false)); logger.trace("JSR-330 'javax.inject.Named' annotation found and supported for component scanning"); } catch (ClassNotFoundException ex) { // JSR-330 API not available - simply skip. } } /** * Set the Environment to use when resolving placeholders and evaluating * {@link Conditional @Conditional}-annotated component classes. *

The default is a {@link StandardEnvironment}. * @param environment the Environment to use */ public void setEnvironment(Environment environment) { Assert.notNull(environment, "Environment must not be null"); this.environment = environment; this.conditionEvaluator = null; } @Override public final Environment getEnvironment() { if (this.environment == null) { this.environment = new StandardEnvironment(); } return this.environment; } /** * Return the {@link BeanDefinitionRegistry} used by this scanner, if any. */ @Nullable protected BeanDefinitionRegistry getRegistry() { return null; } /** * Set the {@link ResourceLoader} to use for resource locations. * This will typically be a {@link ResourcePatternResolver} implementation. *

Default is a {@code PathMatchingResourcePatternResolver}, also capable of * resource pattern resolving through the {@code ResourcePatternResolver} interface. * @see org.springframework.core.io.support.ResourcePatternResolver * @see org.springframework.core.io.support.PathMatchingResourcePatternResolver */ @Override public void setResourceLoader(@Nullable ResourceLoader resourceLoader) { this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); this.componentsIndex = CandidateComponentsIndexLoader.loadIndex(this.resourcePatternResolver.getClassLoader()); } /** * Return the ResourceLoader that this component provider uses. */ public final ResourceLoader getResourceLoader() { return getResourcePatternResolver(); } private ResourcePatternResolver getResourcePatternResolver() { if (this.resourcePatternResolver == null) { this.resourcePatternResolver = new PathMatchingResourcePatternResolver(); } return this.resourcePatternResolver; } /** * Set the {@link MetadataReaderFactory} to use. *

Default is a {@link CachingMetadataReaderFactory} for the specified * {@linkplain #setResourceLoader resource loader}. *

Call this setter method after {@link #setResourceLoader} in order * for the given MetadataReaderFactory to override the default factory. */ public void setMetadataReaderFactory(MetadataReaderFactory metadataReaderFactory) { this.metadataReaderFactory = metadataReaderFactory; } /** * Return the MetadataReaderFactory used by this component provider. */ public final MetadataReaderFactory getMetadataReaderFactory() { if (this.metadataReaderFactory == null) { this.metadataReaderFactory = new CachingMetadataReaderFactory(); } return this.metadataReaderFactory; } /** * Scan the component index or class path for candidate components. * @param basePackage the package to check for annotated classes * @return a corresponding Set of autodetected bean definitions */ public Set findCandidateComponents(String basePackage) { if (this.componentsIndex != null && indexSupportsIncludeFilters()) { return addCandidateComponentsFromIndex(this.componentsIndex, basePackage); } else { return scanCandidateComponents(basePackage); } } /** * Determine if the component index can be used by this instance. * @return {@code true} if the index is available and the configuration of this * instance is supported by it, {@code false} otherwise * @since 5.0 */ private boolean indexSupportsIncludeFilters() { for (TypeFilter includeFilter : this.includeFilters) { if (!indexSupportsIncludeFilter(includeFilter)) { return false; } } return true; } /** * Determine if the specified include {@link TypeFilter} is supported by the index. * @param filter the filter to check * @return whether the index supports this include filter * @since 5.0 * @see #extractStereotype(TypeFilter) */ private boolean indexSupportsIncludeFilter(TypeFilter filter) { if (filter instanceof AnnotationTypeFilter annotationTypeFilter) { Class annotationType = annotationTypeFilter.getAnnotationType(); return (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, annotationType) || annotationType.getName().startsWith("jakarta.") || annotationType.getName().startsWith("javax.")); } if (filter instanceof AssignableTypeFilter assignableTypeFilter) { Class target = assignableTypeFilter.getTargetType(); return AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, target); } return false; } /** * Extract the stereotype to use for the specified compatible filter. * @param filter the filter to handle * @return the stereotype in the index matching this filter * @since 5.0 * @see #indexSupportsIncludeFilter(TypeFilter) */ @Nullable private String extractStereotype(TypeFilter filter) { if (filter instanceof AnnotationTypeFilter annotationTypeFilter) { return annotationTypeFilter.getAnnotationType().getName(); } if (filter instanceof AssignableTypeFilter assignableTypeFilter) { return assignableTypeFilter.getTargetType().getName(); } return null; } private Set addCandidateComponentsFromIndex(CandidateComponentsIndex index, String basePackage) { Set candidates = new LinkedHashSet<>(); try { Set types = new HashSet<>(); for (TypeFilter filter : this.includeFilters) { String stereotype = extractStereotype(filter); if (stereotype == null) { throw new IllegalArgumentException("Failed to extract stereotype from " + filter); } types.addAll(index.getCandidateTypes(basePackage, stereotype)); } boolean traceEnabled = logger.isTraceEnabled(); boolean debugEnabled = logger.isDebugEnabled(); for (String type : types) { MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(type); if (isCandidateComponent(metadataReader)) { ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); sbd.setSource(metadataReader.getResource()); if (isCandidateComponent(sbd)) { if (debugEnabled) { logger.debug("Using candidate component class from index: " + type); } candidates.add(sbd); } else { if (debugEnabled) { logger.debug("Ignored because not a concrete top-level class: " + type); } } } else { if (traceEnabled) { logger.trace("Ignored because matching an exclude filter: " + type); } } } } catch (IOException ex) { throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex); } return candidates; } private Set scanCandidateComponents(String basePackage) { Set candidates = new LinkedHashSet<>(); try { String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) + '/' + this.resourcePattern; Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath); boolean traceEnabled = logger.isTraceEnabled(); boolean debugEnabled = logger.isDebugEnabled(); for (Resource resource : resources) { String filename = resource.getFilename(); if (filename != null && filename.contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) { // Ignore CGLIB-generated classes in the classpath continue; } if (traceEnabled) { logger.trace("Scanning " + resource); } try { MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource); if (isCandidateComponent(metadataReader)) { ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); sbd.setSource(resource); if (isCandidateComponent(sbd)) { if (debugEnabled) { logger.debug("Identified candidate component class: " + resource); } candidates.add(sbd); } else { if (debugEnabled) { logger.debug("Ignored because not a concrete top-level class: " + resource); } } } else { if (traceEnabled) { logger.trace("Ignored because not matching any filter: " + resource); } } } catch (FileNotFoundException ex) { if (traceEnabled) { logger.trace("Ignored non-readable " + resource + ": " + ex.getMessage()); } } catch (ClassFormatException ex) { if (shouldIgnoreClassFormatException) { if (debugEnabled) { logger.debug("Ignored incompatible class format in " + resource + ": " + ex.getMessage()); } } else { throw new BeanDefinitionStoreException("Incompatible class format in " + resource + ": set system property 'spring.classformat.ignore' to 'true' " + "if you mean to ignore such files during classpath scanning", ex); } } catch (Throwable ex) { throw new BeanDefinitionStoreException("Failed to read candidate component class: " + resource, ex); } } } catch (IOException ex) { throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex); } return candidates; } /** * Resolve the specified base package into a pattern specification for * the package search path. *

The default implementation resolves placeholders against system properties, * and converts a "."-based package path to a "/"-based resource path. * @param basePackage the base package as specified by the user * @return the pattern specification to be used for package searching */ protected String resolveBasePackage(String basePackage) { return ClassUtils.convertClassNameToResourcePath(getEnvironment().resolveRequiredPlaceholders(basePackage)); } /** * Determine whether the given class does not match any exclude filter * and does match at least one include filter. * @param metadataReader the ASM ClassReader for the class * @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())) { return false; } } for (TypeFilter tf : this.includeFilters) { if (tf.match(metadataReader, getMetadataReaderFactory())) { return isConditionMatch(metadataReader); } } return false; } /** * Determine whether the given class is a candidate component based on any * {@code @Conditional} annotations. * @param metadataReader the ASM ClassReader for the class * @return whether the class qualifies as a candidate component */ private boolean isConditionMatch(MetadataReader metadataReader) { if (this.conditionEvaluator == null) { this.conditionEvaluator = new ConditionEvaluator(getRegistry(), this.environment, this.resourcePatternResolver); } return !this.conditionEvaluator.shouldSkip(metadataReader.getAnnotationMetadata()); } /** * Determine whether the given bean definition qualifies as candidate. *

The default implementation checks whether the class is not an interface * and not dependent on an enclosing class. *

Can be overridden in subclasses. * @param beanDefinition the bean definition to check * @return whether the bean definition qualifies as a candidate component */ protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { AnnotationMetadata metadata = beanDefinition.getMetadata(); return (metadata.isIndependent() && (metadata.isConcrete() || (metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName())))); } /** * Clear the local metadata cache, if any, removing all cached class metadata. */ public void clearCache() { if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory cmrf) { // Clear cache in externally provided MetadataReaderFactory; this is a no-op // for a shared cache since it'll be cleared by the ApplicationContext. cmrf.clearCache(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy