SpringBoot2自动配置原理及源码解析
1、引言
SpringBoot流行之前,程序员大多是用SSM框架整合来进行WEB后端开发。这种方式非常麻烦,需要手动引入大量的包,还要配置很多XML文件,光是搭建环境就需要很久。
随着“约定大于配置”理念的流行,SpringBoot随之兴起,它大大简化了web开发的流程,可以让初学者快速上手。SpringBoot的核心理念大致有3点:
- 帮助开发者快速整合第3方框架,原理是maven依赖封装和自定义的Starter。
- 完全去除XML,采用纯注解的方式。原理是SpringBoot其实是根据Spring的体系原生的注解实现的包装。
- 不需要外部容器,转而使用内嵌的web容器,原理是使用Java语言创建tomcat服务器,然后将本地的class文件交给tomcat来加载。
Spring Boot是通过各种场景启动器starter
来帮助我们自动引入相关Jar
包依赖并对其版本进行控制的,Spring Boot完成此项功能的主要原理就是自动配置。那么它是怎么实现的呢?
项目基本结构:
完整的基础pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lemon</groupId>
<artifactId>SpringBoot-01</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.6.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
复制代码
2、Spring Boot 2.x 原理分析
2.1、@SpringBootApplication:
首先我们先看一下Spring Boot主程序:这也是我们开发任何一个Spring Boot项目,都会用到的启动类
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
复制代码
在这里我们可以看到注解@SpringBootApplication:此核心复合注解是用于标注在Spring Boot主程序类上,表明该类是Spring Boot执行入口,同时也是表明该项目是一个Spring Boot 项目的标志之一。 点开此注解:
我们可以看到该注解【注解是接口形式】同时标注几个注解,并且@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan这3个注解等同于@SpringBootApplication注解,其中@ComponentScan注解指定扫描哪些Spring注解;已经说过前面
我们来重点分析这3个注解,这些注解具体作用如下所示
2.1.1、@SpringBootConfiguration
@SpringBootConfiguration:SpringBoot配置类注解,由于标注了@Configuration
,所以表明标注了该注解的类是SpringBoot的配置类,将会把被该注解标注的类注册到Spring IOC容器中。因此我们的主程序也是一个配置类,还是一个核心配置类
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
复制代码
@Configuration
:Spring配置类注解,相当于Spring.xml
中的标签,其内部标注着@Component
注解代表当前是一个配置类,说明其本质上是就是Spring容器中的一个组件。
2.1.2、@ComponentScan
参考原来的spring,该注解就是指定扫描那些包,将包中的类自动spring框架自动创建对象,作为组件存放到容器中
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
复制代码
实例:下面的注解中的属性定义了两个扫描器,
@ComponentScan(
excludeFilters = {
@Filter(type = FilterType.CUSTOM,classes = {TypeExcludeFilter.class}),
@Filter(type = FilterType.CUSTOM,classes ={AutoConfigurationExcludeFilter.class})
}
)
复制代码
2.1.3、@EnableAutoConfiguration【核心】
EnableAutoConfiguration:开启自动配置功能的注解。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
复制代码
在这里我们可以看到两个重要注解:@AutoConfigurationPackage
/@Import
,他们是@EnableAutoConfiguration
的合成注解
@AutoConfigurationPackage
@AutoConfigurationPackage:自动开启包配置,指定了默认的包的配置管理的规则
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
复制代码
我们可以发现该注解是通过@Import注解导入Registrar 类这一个组件,从而利用Registrar给容器中将当前主程序类的同级以及子级的包里面的所有组件都导入进来【批量注册】。那是怎样导入这一系列组件的呢?如下: 点进去Registrar.class
类
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}
//给容器批量注册组件
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
}
}
复制代码
其中里面有一个方法:AutoConfigurationPackages.register()
,这个方法就是批量注册所有包里面的组件
Debug这个方法查看批量注册原理:
- 其中,我们发现
registerBeanDefinitions
这个方法有一个参数AnnotationMetadata metadata
- 其中
AnnotationMetadata metadata
是注解@AutoConfigurationPackage
的元信息,代表@AutoConfigurationPackage
这个注解标在了哪里,里面的属性值是什么,如下:这里是标注在主程序类上
其次,AutoConfigurationPackages.register()
方法里面有这么一个参数,其中又发现这么一段代码:
(String[])
(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0])
复制代码
其中:new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames()
:就是把@AutoConfigurationPackage
注解的元信息拿到,从而得到包名,验证:选中该代码,右键Evaluate
从结果发现:返回得到的是一个包名[主程序所在的包名]
最终通过toArray(new String[0]));
把包名封装到一个数组里面,然后就全部注册进去,因此得出前面的结论:
@AutoConfigurationPackage
注解是通过@Import
注解导入Registrar
类这一个组件,从而利用Registrar
给容器中将当前主程序类的同级以及子级的包里面的所有组件都导入进来【批量注册】,这就解决了为什么当初说的是默认扫描的是主程序所在的包及其子包的问题
@Import(AutoConfigurationImportSelector.class)
前面的@AutoConfigurationPackage
确定了导入容器的是主程序所在包及其子包下的组件。那么具体导入的是一些什么组件呢?因此这个注解就能帮我们解决这个问题。
@Import
: 导入自动配置的组件AutoConfigurationImportSelector
:自动配置选择器,这就是我们自动配置的核心类,通过该类,Spring Boot确定将会把哪些组件[131个]加入到Spring容器中。其核心方法是selectImports。点进去源码分析,找到核心方法
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
复制代码
发现里面有个getAutoConfigurationEntry(annotationMetadata)
方法,**就是利用这个方法给容器中批量导入具体的一些组件进去,**往下翻,找到该方法的具体实现,Debug调试该方法:
一直放行,放行到如下图所示的代码 :
其中getCandidateConfigurations(annotationMetadata, attributes)
就是获取所有候选的配置,以集合的形式存放,即得到configurations的list集合,从下面代码看出该configurations
用到了很多处[比如removeDuplicates移除一些重复的、checkExcludedClasses排除一些东西….然后封装成对象,最终返回]。可以发现,这个configurations很重要,共131个
说明这131个组件是全部都要默认导入到容器中的。
那么导入的原理是什么呢?debug 调试List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
Step into进入该方法
可以发现他是利用SpringFactoriesLoader工厂加载了一些东西,点进去
SpringFactoriesLoader.loadFactoryNames()
方法,如下:
又发现最后返回了一些东西,点进去该loadSpringFactories方法,如下,因此最后就是利用Map<String, List<String>> loadSpringFactories(ClassLoader classLoader)
加载得到所有的组件,那么是从哪里加载的呢?可以看见是从META-INF/spring.factories
位置来加载一个文件的,从而默认扫描我们当前系统里面所有META-INF/spring.factories
位置的文件
以Springboot的包为例:
除了这个包,我们最核心的包spring-boot-autoconfigure-2.5.2.jar
也有指定的文件
其中这里面就是我们的131个自动配置类,该文件里面写死了springboot一起动就要给容器中加载的所有配置类
2.2、流程总结:
-
利用
getAutoConfigurationEntry(annotationMetadata);
给容器中批量导入一些组件,131个 -
调用
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
获取到所有需要导入到容器的配置类(组件) -
利用
Map<String, List<String>> loadSpringFactories(ClassLoader classLoader)
加载得到所有的组件,那么是从哪里加载的呢?可以看见是从META-INF/spring.factories
位置来加载一个文件的,从而默认扫描我们当前系统里面所有META-INF/spring.factories
结论:
springboot的所有自动配置都是在启动的时候扫描并加载核心包下的/META-INF/spring.factories
文件,但是不一定生效,要判断条件是否成立,只要导入了对应的start,就有了对应的启动器,有了启动器,我们的自动装配,就会生效,然后就配置成功了
步骤:
-
springboot在启动的时候,从类路径下
/META-INF/spring.factories
获取指定的值 -
将这些自动配置的类导入容器,自动配置类就会生效,帮我们自动配置
-
以前我们需要自动配置的东西,现在springboot帮我们做了
-
整个javaEE,解决方案和自动配置的东西都在spring-boot-autoconfigure-2.5.2.jar里面
-
它会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器
-
容器中也会存在非常多的
XXXAutoConfiguration
的文件,就是这些类给容器中导入这些场景需要的所有的组件并自动配置 -
有了自动配置类,免去了我们手动编写配置文件的工作
2.3、按需加载,条件装配
我们都知道spring.factories
这个文件中有所有的自动配置类,我们前面也测试过,不是所有的自动配置类都生效,为什么这么多配置类中有的文件没有生效,而需要导入对应的start
才能起作用? 以aop下面这个AopAutoConfiguration
为例,它也是我们默认加载进容器注册的组件类之一,但是我们发现容器里没有这个组件,原因就是因为条件规则的缘故
虽然我们131个场景的所有自动配置启动的时候默认全部加载,但是按照条件装配规则注解(@ConditionOn XXX
,如果这里面的条件都满足才会生效!),最终会按需配置,不是全部都加载到容器里面去,即必须按照条件说明的才行。
由于我们这次的演示项目并没有引入aop相关场景的依赖jar包,我们所以我们的Aop切面功能并不能实现,就是因为条件规则的原因:如下图所示,必须满足条件才行,即我们的类路径里面存在·Advice.class·这个类,下面的jdk代理等才能生效,aop功能才能生效[pom文件中要有aop场景]
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({Advice.class})
static class AspectJAutoProxyingConfiguration {
AspectJAutoProxyingConfiguration() {
}
@Configuration(
proxyBeanMethods = false
)
@EnableAspectJAutoProxy(
proxyTargetClass = true
)
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"proxy-target-class"},
havingValue = "true",
matchIfMissing = true
)
static class CglibAutoProxyConfiguration {
CglibAutoProxyConfiguration() {
}
}
@Configuration(
proxyBeanMethods = false
)
@EnableAspectJAutoProxy(
proxyTargetClass = false
)
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"proxy-target-class"},
havingValue = "false"
)
static class JdkDynamicAutoProxyConfiguration {
JdkDynamicAutoProxyConfiguration() {
}
}
}
复制代码
补充:autoconfig配置类中一些注解的作用:
3、SpringBoot2自动配置流程演示【面试重点】
以SpringBoot自动配置jar包下的autoconfigure包下的aop下面这个AopAutoConfiguration为例,它也是我们SpringBoot一启动就默认加载进容器注册的组件类之一,但是我们发现容器里没有这个组件,原因就是因为条件规则的缘故
我们现在来分析它的自动配置流程,同时也演示一些生效的配置类的工作流程,下图是这个类的源码
//第一部分
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"auto"},
havingValue = "true",
matchIfMissing = true
)
public class AopAutoConfiguration {
public AopAutoConfiguration() {
}
//第二部分
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass({"org.aspectj.weaver.Advice"})
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"proxy-target-class"},
havingValue = "true",
matchIfMissing = true
)
static class ClassProxyingConfiguration {
ClassProxyingConfiguration() {
}
@Bean
static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() {
return (beanFactory) -> {
if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry)beanFactory;
AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
};
}
}
//第三部分
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({Advice.class})
static class AspectJAutoProxyingConfiguration {
AspectJAutoProxyingConfiguration() {
}
@Configuration(
proxyBeanMethods = false
)
@EnableAspectJAutoProxy(
proxyTargetClass = true
)
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"proxy-target-class"},
havingValue = "true",
matchIfMissing = true
)
static class CglibAutoProxyConfiguration {
CglibAutoProxyConfiguration() {
}
}
@Configuration(
proxyBeanMethods = false
)
@EnableAspectJAutoProxy(
proxyTargetClass = false
)
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"proxy-target-class"},
havingValue = "false"
)
static class JdkDynamicAutoProxyConfiguration {
JdkDynamicAutoProxyConfiguration() {
}
}
}
}
复制代码
3.1、AopAutoConfiguration组件是否生效–按需加载
1、首先:观看源码第一部分:如下:首先该类标注了@Configuration,说明是个配置类【我们要加载进容器的要的就是配置类】,且有一个条件注解@ConditionalOnProperty,里面的参数说明了:判断配置文件xxxxProperties文件中是否存在前缀为spring.aop的这个配置,并且仅在属性 spring.aop.auto 存在(matchIfMissing=true:就算你没配置,我也认为你配了)或者明确指定为 true 时生效,因此AopAutoConfiguration这个组件里面的代码生效。
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"auto"},
havingValue = "true",
matchIfMissing = true
)
public class AopAutoConfiguration {
public AopAutoConfiguration() {
}
复制代码
2、查看第三部分内容
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({Advice.class})
static class AspectJAutoProxyingConfiguration {
AspectJAutoProxyingConfiguration() {
}
@Configuration(
proxyBeanMethods = false
)
@EnableAspectJAutoProxy(
proxyTargetClass = true
)
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"proxy-target-class"},
havingValue = "true",
matchIfMissing = true
)
static class CglibAutoProxyConfiguration {
CglibAutoProxyConfiguration() {
}
}
@Configuration(
proxyBeanMethods = false
)
@EnableAspectJAutoProxy(
proxyTargetClass = false
)
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"proxy-target-class"},
havingValue = "false"
)
static class JdkDynamicAutoProxyConfiguration {
JdkDynamicAutoProxyConfiguration() {
}
}
}
复制代码
首先,它还是一个配置类,有一个条件注解@ConditionalOnClass,参数说明了 Advice.class存在于 classpath 时才会生效,我们使用ctrl+N搜索Jar包里的Advice类,发现没有这个类,注意我们这个Advice类是存在于aspectj包下的【import org.aspectj.weaver.Advice;】并且由于我们没有导入aspectj包,所以这部分的这个类的代码不会生效
3、查看第二部分内容,即上上图中的第一个static class部分
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnMissingClass({"org.aspectj.weaver.Advice"})
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"proxy-target-class"},
havingValue = "true",
matchIfMissing = true
)
static class ClassProxyingConfiguration {
ClassProxyingConfiguration() {
}
@Bean
static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() {
return (beanFactory) -> {
if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry)beanFactory;
AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
};
}
}
复制代码
首先,它也是一个配置类,有一个条件注解@ConditionalOnMissingClass,参数说明了当系统类路径里面没有 org.aspectj.weaver.Advide这个类时该代码才会生效【我们就是由于恰好没有这个advice类,我们的这部分的这个类才会生效】,然后又来判断另外一个注解@ConditionalOnProperty,参数说明在配置参数 spring.aop.proxy-target-class 存在或者值被明确设置为 true 时生效,所以该代码生效,它默认是开启了aop功能,是一个简单的aop功能。[有接口有注解]
总结:在我们的条件规则下:aop自动配置类里面有些生效有些不生效。所以综上,AopAutoConfiguration这个配置类组件不生效,因为没有导入aspectj包,不存在Advice类,但是可以完成简单的AOP功能
3.2、修改默认配置
以SpringMVC为例,引入了web场景
再来看这个自动配置类HttpEncodingAutoConfiguration
SpringBoot默认会在底层配好所有的组件【比如我们放入字符编码,字符解析器名字,也防止你瞎用】。但是如果用户自己配置了以用户的优先:比如mvc的字符编码器。 约定大于配置
原生配置类上有@ConditionalOnMissingBean
这个注解就可以自己配置,如下所示: 原生的不生效,就使用我自己定义的过滤器,加载到容器中
@Bean
public CharacterEncodingFilter filter( ){
return null;
}
复制代码
3.3、总结:
- SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration
- 每个自动配置类按照条件规则进行生效,按需配置。并且默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties又和配置文件appliction.properties进行了绑定
- 生效的配置类就会给容器中装配很多组件
- 只要容器中有这些组件,相当于这些功能就有了
- 用户配置类就自己生效,
- 定制化配置
- 法1:用户直接自己
@Bean
替换底层的组件 - 法2:用户去看这个组件是获取的配置文件什么值就去修改配置文件即可。
- 比如下面的修改字符编码
- 法1:用户直接自己
具体的每个配置的描述可以在下面查看
演示:
比如我要配置缓存
各个参数详解
4、spring boot 使用及最佳实践
4.1、SpringBoot开发实践
以后我们要进行SpringBoot项目的开发,遵循以下步骤即可
-
引入相关场景依赖【包括第三方】
-
比如WEB开发引入web场景依赖即可
-
-
查看自动配置了哪些(选做)
- 方法1:自己分析,引入场景对应的自动配置一般都生效
- 方法2:配置文件
application.properties
中写上debug=true
开启自动配置报告。Negative(不生效)\Positive(生效)【会把生效的和没生效的都列出来】
-
是否需要修改
-
参照文档修改配置项:docs.spring.io/spring-boot…
-
默认名是banner.txt.banner.gif等,不是这个就修改就行
- 类路径:Classpath指的是resource目录下
- 自己分析:xxxxProperties绑定了配置文件的哪些前缀。
- 自定义加入或者替换组件@Bean、@Component。。。
- 自定义器 XXXXXCustomizer;
……