本文是Spring IoC容器技术介绍系列文章之一。本文介绍@ComponentScan
。
@ComponentScan
注解在Spring中用来定义IoC容器需要扫描哪些类文件。在这些类文件中,所有以@Component
标注的对象和以@Bean
标注的方法都会被自动加载到IoC容器中。
另外,除了@Component
注解之外,其他诸如@Controller
、@Configuration
、@Service
这些继承了@Component
注解的注解,也同样适用于上述的扫描规则,均会被自动加载到IoC容器中。
IoC容器在启动时,需要指定一个或多个配置类。这些配置类会在IoC容器初始化之前,被解析成一个个BeanDefinition
对象。正常使用时,一般会在这些配置类上增加@Configuration
注解,表明他们是用于容器配置的。但实际上,@Configuration
注解是可以省略的。这是因为,这些配置类是通过手动设置的方式,由IoC直接加载进容器,而并未使用到自动扫描机制。
比如说,下面这段代码是完全有效的:
public class Application {
@Bean NumberStyleFormatter numberStyleFormatter() {
return new NumberStyleFormatter();
}
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Application.class);
NumberStyleFormatter numberStyleFormatter = applicationContext.getBean(NumberStyleFormatter.class);
System.out.println(numberStyleFormatter);
}
}
但即使如此,我们仍建议在这些类上增加@Configuration
注解,便于理解。
默认情况下,只有如下的bean对象会被加载到IoC容器中:
配置类里面使用@Component
标注的静态内部类;
配置类里面使用@Bean
标注的返回不为void
类型的方法;
配置类上通过@Import
注解引入的其他类;
当需要IoC容器加载除了上述三种情况之外的其他Bean对象时,则需要使用@ComponentScan
注解进行标注。比如下面的代码会让IoC容器扫描并加载org.example.app.bean
包下的所有bean对象。
@ComponentScan("org.example.app.bean")
public class Application {
@Bean NumberStyleFormatter numberStyleFormatter() {
return new NumberStyleFormatter();
}
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Application.class);
NumberStyleFormatter numberStyleFormatter = applicationContext.getBean(NumberStyleFormatter.class);
System.out.println(numberStyleFormatter);
}
}
如果@ComponentScan
注解不带参数,则默认扫描路径是以标注了该注解的类所在包为根路径,其下的所有类和子包下的所有类。
The @SpringBootApplication annotation is often placed on your main class, and it implicitly defines a base “search package” for certain items.
Spring Boot中指出,如果将@SpringBootApplication
注解放在启动类上,并让启动类放在工程的最外层的包下面,则会自动扫描所有类文件来加载bean。这个机制的原理是:@SpringBootApplication
继承了@ComponentScan
注解。所以,@SpringBootApplication
在没有指定扫描包路径时,会从当前位置一直往下扫描。
...
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(...)
public @interface SpringBootApplication {
...
}
Spring框架在初始化加载IoC容器时,会先根据配置类的配置信息,找到所有需要加载到IoC容器中@Component
类型的类和@Bean
类型的方法。它会为这些类和方法创建类型为BeanDefinition
的对象,每个类和方法都会创建一个独立的BeanDefinition
对象。BeanDefinition
中包含了创建这些bean对象时需要使用的相关信息。
为了保证能将符合要求的@Component
和@Bean
对象都找到,Spring IoC容器在启动时,会对每个@Component
对象都执行如下操作:
确认该对象有没有静态内部类,如果有则确认其是否是一个合法的bean,若是则将其也加载到IoC容器中;
确认该对象有没有使用@Bean
标注的方法,如果有则加载到IoC容器中;
确认该类有没有使用@ComponentScan
注解标注,如果有则开始扫描相关路径,将得到的合法的bean对象也加载的IoC容器中
……
其中的第3步便涉及到对@ComponentScan
注解的处理。
@Nullable
protected final SourceClass doProcessConfigurationClass(
...
// Process any @ComponentScan annotations
Set componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
...
}
上面这段源码中,通过this.componentScanParser.parse(...)
方法拿到了扫描后的所有bean对象。这部分就是Spring IoC处理@ComponentScan
的核心代码。
现在我们还有最后一个问题没有回答。如果@ComponentScan
中没有指定扫描路径,程序是怎么确定扫描根路径的?
public Set parse(AnnotationAttributes componentScan, final String declaringClass) {
...
Set basePackages = new LinkedHashSet<>();
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
for (Class> clazz : componentScan.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
basePackages.add(ClassUtils.getPackageName(declaringClass));
}
...
return scanner.doScan(StringUtils.toStringArray(basePackages));
}
这种场景的处理逻辑就在上面这段源码中。当basePackages是空的时候,使用declaringClass的包作为扫描的根路径,basePackages.add(ClassUtils.getPackageName(declaringClass))
。
本文主要介绍了Spring IoC容器中,@ComponentScan
的使用方法,以及背后的实现逻辑。