文章目录
前言
在常规的SSM项目中,通常需要用户自己去进行Spring、Spring MVC、Mybatis的整合,以及Tomcat的配置。而在Spring Boot项目中,整合工作已经由Spring Boot去完成,通常我们会引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
的依赖,在spring-boot-starter-web
中就已经完成了Spring、Spring MVC、Tomcat的整合,通常被称为自动配置
。
而完成
自动配置
的关键,在于启动类上的@SpringBootApplication
注解:
@SpringBootApplication
是一个复合注解,其中包含了@SpringBootConfiguration
、@EnableAutoConfiguration
和@ComponentScan
一、@SpringBootConfiguration
@SpringBootConfiguration
注解,没什么好说的,主要作用是将标注了该注解的类标记为配置类(通常是启动类),并且该配置类还会作为一个Bean放入Spring容器中。
@Configuration
注解包含了@Component
。
二、@EnableAutoConfiguration
@EnableAutoConfiguration
是三个注解中最重要的一个,自动配置的源码就在该注解中有所体现,它也是一个复合注解,包含了@AutoConfigurationPackage
和@Import
。
2.1、@AutoConfigurationPackage
@AutoConfigurationPackage
上使用了@Import
注解,导入了AutoConfigurationPackages
类,当执行该类的register
方法时(执行时机:Spring的refresh方法扫描配置类并解析时),会尝试向bean定义中注册一个类型为BasePackagesBeanDefinition
的Bean,其中的basePackages
属性存放的就是当前配置类所在的包路径。
该包路径可以给其他jar包使用,例如MyBatis的扫描逻辑。
2.2、@Import(AutoConfigurationImportSelector.class)
在@EnableAutoConfiguration
中,还使用@Import
注解去导入了AutoConfigurationImportSelector
类,该类中的逻辑是自动配置的关键:
AutoConfigurationImportSelector
类实现了DeferredImportSelector
接口。DeferredImportSelector
从名称上看,似乎是在ImportSelector
的基础上增加了延迟的功能,实际上DeferredImportSelector
也是继承了ImportSelector
。
为什么要实现
DeferredImportSelector
接口?先看一个案例,这个案例的目的是进行条件装配,假如项目中没有自定义的User对象,就在配置类中创建一个User。
public class RagdollCatConfiguration {
@Bean
@ConditionalOnMissingBean(User.class)
public User user(){
return new User("从RagdollCatConfiguration创建");
}
}
public class RagdollCatImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.itbaima.boot.RagdollCatConfiguration"};
}
}
@SpringBootApplication
@Import(RagdollCatImportSelector.class)
public class StartApplication {
@Bean
public User user(){
return new User("从StartApplication创建");
}
public static void main(String[] args) {
SpringApplication.run(StartApplication.class,args);
}
}
预期的结果是,控制台打印从RagdollCatConfiguration创建
。但是最终的结果是报错:
原因在于Spring的refresh方法中,解析配置文件的顺序问题:
解析@Import注解
在ConfigurationClassParser#processImports
中,因为案例中的RagdollCatImportSelector
没有实现DeferredImportSelector
接口,所以走的是else的逻辑,即执行RagdollCatImportSelector
的select方法,拿到配置类路径,然后解析配置类RagdollCatConfiguration
的内容。
也就是说,
RagdollCatConfiguration
中的代码,会先于StartApplication
中的执行,而在执行RagdollCatConfiguration
中的条件装配时,Spring容器中肯定是没有User的,在这里就会创建。而在StartApplication
中尝试再次将User注册成Bean,就会出现上面的错误。
这样的结果很明显不符合预期,我们是希望用户自定义Bean的优先级,要高于Spring Boot条件装配的优先级。解决的方法在上图中也有所体现,即实现DeferredImportSelector
接口:
public class RagdollCatImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.itbaima.boot.RagdollCatConfiguration"};
}
}
最终的效果是用户自定义的Bean先于Spring Boot的条件装配执行:
但是在AutoConfigurationImportSelector
中,执行的并非是selectImports
的逻辑。
因为该类同时还重写了
getImportGroup
方法:
实际执行的是
AutoConfigurationGroup
的process
方法:
而
process
方法,最终同样会调用到selectImports
中的getAutoConfigurationEntry
方法,该方法是自动配置的核心方法。
三、@ComponentScan
@ComponentScan
是进行路径扫描的注解,如果没有指定路径,默认是扫描标注了@ComponentScan
注解的类,所在包下的所有类。而重点要看的是其中的两个排除条件:
第一个排除条件,是用户自定义了实现
TypeExcludeFilter
的类,然后重写了其中的match方法:
public class MyACI implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.getBeanFactory().registerSingleton("typeExcludeFilter",new TypeExcludeFilter(){
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return metadataReader.getClassMetadata().getClassName().equals("com.itbaima.boot.User");
}
});
}
}
满足条件即被排除:
第二个排除条件,是排除掉自动配置类:
四、自动配置源码
selectImports
中的getAutoConfigurationEntry
方法是自动配置的核心源码:
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
// 1. 检查 @EnableAutoConfiguration 是否启用
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 2. 获取 @EnableAutoConfiguration 注解的属性(如果有的话)
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 3. 获取所有候选的自动配置类(从 META-INF/spring.factories 或 AutoConfiguration.imports 加载)
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 4. 去重,防止重复加载
configurations = removeDuplicates(configurations);
// 5. 获取所有被排除的自动配置类(@EnableAutoConfiguration(exclude = xxx.class) 或 spring.autoconfigure.exclude 配置)
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 6. 校验是否有错误的排除类(比如排除的类根本不存在)
checkExcludedClasses(configurations, exclusions);
// 7. 从候选配置中移除排除的配置类
configurations.removeAll(exclusions);
// 8. 过滤不满足条件的自动配置(基于 @ConditionalOnClass、@ConditionalOnProperty 等条件)
configurations = getConfigurationClassFilter().filter(configurations);
// 9. 触发自动配置导入事件(给监听器用)
fireAutoConfigurationImportEvents(configurations, exclusions);
// 10. 返回最终的自动配置类列表
return new AutoConfigurationEntry(configurations, exclusions);
}
重点看一下其中的3和8。
4.1、获取所有候选的自动配置类
Spring Boot是如何知道那些jar包需要自动装配的?首先在getCandidateConfigurations
方法中,会从spring.factories
文件中获取所有的自动配置类(也会获取自己项目中定义的spring.factories
文件中的,以及第三方jar包中META-INF的spring.factories
):
spring-boot-autoconfigure中的
spring.factories
:
点进去发现都是一些条件装配类。重点看类上的
@ConditionalOnClass
注解。只有当前项目的依赖中有@ConditionalOnClass
注解标注的类,该配置才会生效,这些类都是对应依赖项的核心类。(例如下图,只有项目中引入了RabbitMQ 相关的依赖,它才会生效)
但是在获取所有候选的自动配置类这一步,会拿到全量的:
4.2、过滤不满足条件的自动配置
在fliter方法中,拿到三个默认的filter,调用match方法进行条件匹配:
对于4.1中拿到的所有自动配置类,进行条件匹配:
在进行类条件匹配时,还会去开启多线程执行:
在这里才会真正去执行
spring.factories
中具体每个配置项的@ConditionalOnClass
等条件装配注解的匹配,会过滤掉项目中没有对应依赖的配置项。
总结
Spring Boot的@SpringBootApplication
是一个复合注解,包含了:
- @SpringBootConfiguration:标记当前类是配置类,并且将当前类加入Spring 容器。
- @EnableAutoConfiguration:将当前类所在的包路径注册到bean定义,并且进行自动配置。(实现了
DeferredImportSelector
接口,以用户自定义的bean优先。) - @ComponentScan:默认扫描当前类所在的包下的所有类。两个排除条件:实现了
TypeExcludeFilter
接口,重写的match方法的逻辑,以及其他自定义的自动配置类。
即:标注了@SpringBootApplication
的类,自身就作为Spring 的配置类。Spring会去扫描该类所在的包下的所有类。当然@SpringBootApplication
也可以不标注在启动类上。并且Spring Boot 自动配置的实现,依靠:
- META-INF下的
spring.factories
文件(自身以及第三方jar包的,可以称为SPI机制) - 条件装配,
@ConditionalOnClass
等注解。
同时在@ConditionalOnClass
的实现中,进行条件装配还会开启多线程执行(2个线程),提高效率。