SpringBoot 自动装配原理浅探
1 pom.xml
1.1 项目依赖管理
打开项目的配置文件 pom.xml,看起来和以前的 ssm 项目差不多,仔细观察发现,以前的依赖都是由 GAV 三部分构成
但是在 SpringBoot 中没有了 V(版本号),这是为什么?
难道依赖没有版本号吗?显然不可能。继续往下找,发现有一个 <dependencyManagement>
的配置,看名字似乎是依赖管理器?
点击进入 spring-boot-dependencies
,我们发现,原来 SpringBoot 将版本号都在 spring-boot-dependencies-xxx.pom
文件里统一进行管理了
1.2 启动器
配置文件中,我们稍微仔细观察,会发现一个规律:好像所有的依赖都是以 spring-boot-starter
开头的?
这应该不是偶然,我们去 SpringBoot [官方文档](Developing with Spring Boot) ,官方这里有对于启动器的描述:
启动器是一个方便于我们在项目中引入依赖的集合
官方启动器的名字都是类似于 spring-boot-starter-*
的形式,这可以很方便的帮助我们快速引入项目所需要的环境以来
比如想要引入关于 aop 的依赖,只需要加上 aop 启动器,即 spring-boot-starter-aop
即可,需要什么功能,只需要加入对应的启动器
2 主启动类的注解
项目的主启动类只有寥寥几行代码,那为什么就能启动整个项目?
package com.jiuxiao;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBoot01Application {
public static void main(String[] args) {
SpringApplication.run(SpringBoot01Application.class, args);
}
}
我们首先通过研究注解来探究 SpringBoot 自动装配的原理
2.1 @SpringBootApplication
这个注解,顾名思义,表明它是一个 SpringBoot 应用,我们点进去该注解,发现有很多的注解,除过那几个无关紧要的元注解之外,值得我们注意的注解有 @SpringBootConfiguration
和 @EnableAutoConfiguration
这两个
2.2 @SpringBootConfiguration
该注解表明它是一个 SpringBoot 的配置,依次点进去该注解
很清楚的可以看到,主启动类上的 @SpringBootConfiguration
注解,本质上是一个 Spring 的组件
2.3 @EnableAutoConfiguration
点进该注解,除过元注解外,有两个重要的注解
- 对于第一个注解,
@AutoConfigurationPackage
,意思就是自动配置包,那么,它配置了什么东西?
再点进去该注解,发现它是导入了一个配置包选择器选择器,导入了什么选择器?
再往里进入,发现 AutoConfigurationPackages.Registrar
注册了一些 bean,然后导入了一些元数据,这些元数据估计与包扫描有关,这里先不深入
- 对于第二个注解,
@Import(AutoConfigurationImportSelector.class)
,它导入了一些选择器
进入 AutoConfigurationImportSelector
选择器 ,里面有一个名为 getAutoConfigurationEntry
的方法,根据名称可以知道,该方法是自动获取配置项目
方法中有一句如下所示代码,它的作用是获取候选配置列表
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
那么怎么获取候选配置列表?点进去 核心方法 getCandidateConfigurations()
方法
首先,看 getCandidateConfigurations()
方法的第一行代码
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader()
);
一共传递了两个参数,第二个参数 getBeanClassLoader()
应该就是使用 bean 加载器加载进来了一些 bean,很好理解
第一个参数 getSpringFactoriesLoaderFactoryClass()
是一个方法,我们去看该方法,该方法只有一个返回值
就是返回了 EnableAutoConfiguration
的 class 文件,这个 EnableAutoConfiguration
有点似曾相识?不正是我们一直在研究的这个注解 @EnableAutoConfiguration
吗?
兜兜转转一圈,我们明白了,@EnableAutoConfiguration
注解作用之一就是为了导入启动类之下的所有资源!
然后再看 getCandidateConfigurations()
方法的第二行代码
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you ...");
它断言了一个配置文件 META-INF/spring.factories
非空,换个角度想想,是不是只要该配置文件非空,它就会被加载?那么我们就去找到该配置文件
我们在项目所有依赖的 jar 包中,找到一个名为 spring-boot-autoconfigure-xxxxx.jar
的包
在它这个 jar 包里面,我们找到了断言处所提到的配置文件 spring.factories
,那么不出意外的话,它应该就是自动配置的核心文件了
打开该文件,我们看看他到底都配置了什么东西?可以看到,配合了很多很多的配置,那么,为什么读取了这个文件后,他就能自动装配好?
我们以我们熟悉的 WebMvc
的配置为例来进行分析,点进去 WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,
我们发现它是一个配置类,在里面我们找到了 ssm 框架中我们所熟悉的一些东西,比如静态资源过滤、视图解析器等等
//静态资源过滤
public void addResourceHandlers() {}
//资源配置链
private void configureResourceChain() {}
//视图解析器
private ResourceResolver getVersionResourceResolver() {}
至此,我们大致明白了,getCandidateConfigurations()
方法,它先通过 EnableAutoConfiguration
的 class 文件,利用反射机制来获取到当前启动类下的所有资源文件,然后再去读取核心配置文件 META-INF/spring.factories
,利用该配置文件中的配置,去找到配置文件中所设计的所有配置类
我们再去看 getCandidateConfigurations()
方法的第一行代码,方法 loadFactoryNames()
里面的两个参数我们刚刚在上面已经分析过,现在看方法本身
点进去 loadFactoryNames()
方法,该方法就做了一件事,调用 loadSpringFactories()
方法,依然去读取 META-INF/spring.factories
这个核心配置文件,然后将获取到的所有资源配置类全部放在一个名为 properties
的配置类中,所以该配置类 properties
就可以直接供我们使用!
以上重要注解的原理图大致如下:
@ComponentScan、@SpringBootConfiguration
@@EnableAutoConfiguration(核心注解)
思考:经过上面的分析,我们已经知道,SpringBoot 会将从
META-INF/spring.factories
中读取并加载的所有配置类全部添加到一个名为properties
的配置类中,供我们直接使用。那么,既然所有的配置类都被加载了,为什么很多都没有生效,需要我们去在 pom.xml 中导入对应的starter
才会生效?
我们去 spring.factories 文件中,随意找一个我们没有使用的配置类,比如下面的 security.reactive.ReactiveSecurityAutoConfiguration
配置类
可以很清楚的看到,编译器中直接报红,这意味着我们这些包并没有进行导入
再看该类上面的 @ConditionalOnClass
注解,该注解的作用就是判断该配置类是否被用户在项目中使用 spring-boot-strater-xxx
的形式引入
如果没有使用 starter
的形式进行引入,则虽然被加载,但不会生效,这也就是为什么全部配置类都被导入了,但只有使用 starter
后才会生效的原因
2.4 注解小结
-
SpringBoot 在启动的时候,会直接从
/META-INF/spring.factories
文件中来获取指定的配置类 -
获取到这些配置类的全限定名之后,就会将这些自动配置类导入 Spring 容器中,接下来 Spring 就会帮助我们进行自动配置
-
在 SpringBoot 项目中,自动装配的方案和配置,都在
spring-boot-autoconfigure-xxxx.jar
这个 jar 包中 -
容器中会存在非常多的
xxxAutoConfiguration
文件(本质仍然是一个个 bean),就是这些自动配置类,给 Spring 容器中导入了各种场景下所需要的组件,并进行了自动装配 -
有了这些自动配置类,就免去了我们自己去编写配置文件的流程
3 主启动类
SpringApplication 类主要做了以下几件事情:
-
推断应用的类型是普通 Java 项目还是 Web 项目
-
查找并初始化所有的可用初始化器,设置到 initlizers 属性中
-
找出所有应用程序的监听器,设置到 listeners 属性中
-
推断并设置 main 方法的定义类,找到运行的主类(通过传入的当前类的 class 文件来推断)
该类的构造器初始化了以下属性,比如资源、控制台日志、注册关机、自定义环境、资源部加载器、初始化器、监听器、主程序类等等
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.sources = new LinkedHashSet();
this.bannerMode = Mode.CONSOLE;
this.logStartupInfo = true;
this.addCommandLineProperties = true;
this.addConversionService = true;
this.headless = true;
this.registerShutdownHook = true;
this.additionalProfiles = new HashSet();
this.isCustomEnvironment = false;
this.lazyInitialization = false;
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = this.deduceMainApplicationClass();
}
4 run 方法流程探究
为了研究 run 方法到底干了什么事,才可以让整个项目启动,我们给 run 方法打上断点一步步进行调试
run 方法执行一共分为三个大阶段:准备启动阶段、正式启动阶段、启动结束阶段
- new SpringApplication()、init 加载初始化
当启动 SpringApplication
之后,首先创建了一个 SpringApplication
的实例
然后去执行 SpringApplication
的构造函数,使用构造函数进行 init()
初始化,一共做了以下四步操作:
-
根据类路径推断应用是否为 Web 项目
-
加载所有可用的初始化器
-
设置所有可用的程序监听器
-
推断并设置 main 方法的定义类
- 开始执行 run() 方法
然后 run() 方法开始准备执行,他首先会实例化一个监听器,这个监听器在启动之后会持续监听应用程序上下文
- step1 : headless 系统属性设置
该阶段中,程序开始设置 headless 相关的系统属性(下方流程图的 step 1)
- step 2 : 初始化监听器 getRunListener(args)
程序将前面实例化的监听器进行初始化设置(下方流程图的 step 2)
然后会使用 SpringFactoriesInstances
来根据传入的对象名来得到所需的工厂对象
这里的对象名,就是从 2.2 中提到的 spring-boot-autoconfigure-xxxx.jar
这个 jar 包下的 /META-INF/spring.factories
文件中所获取的
这个文件中配置了所有的自动配置对象的全限定名,工厂对象会根据该对象的 class 文件,使用反射机制得到该对象的构造犯法,最后生成一个工厂的实例对象并返回
- step 3 : 启动准备好的监听器
然后将初始化完成的监听器正式启动
这个监听器会持续监听上下文,直到上下文发布完成并返回之后,它才会停止监听(下方流程图的 step 3)
- step 4 : DefaultApplicationArguments
开始装配环境参数,创建了 web/standard 环境、 加载了属性源、加载了预监听集合
到此步骤为止,应用的 准备启动阶段 已经完成!(下方流程图的 step 4)
- step 5 : 打印 banner 图案
这一步开始,应用正式开始启动,首先会打印 banner 图案(下方流程图的 step 5)
- step 6/6.1 : 上下文区域、根据类型创建上下文
到了这里就开始创建上下文区域
程序会根据 web/standard
的类型来创建与之对应的上下文(下方流程图的 step 6、step 6.1)
- step 7 : 准备上下文异常报告
这一步骤中,程序会根据 SpringFactoriesInstances
来创建对应的上下文异常报告(下方流程图的 step 7)
- step 8 : 上下文前置处理 prepareContext
该步骤会对上下文进行前置处理,包括监听器的配置、相关环境配置、初始化器设置、资源加载等操作
至此,上下文的前置准备工作结束(下方流程图的 step 8)
- step 9 : 上下文刷新 refreshContext
step 8 中上下文初始化完成之后,接下来就是给上下文中写入东西(刷新上下文)
在该步骤中,程序会加载 bean 工厂、生产所有的 bean、完成 bean 工厂的初始化,最后再次刷新生命周期(下方流程图的 step 9)
关于上下文的所有操作结束以后,程序启动阶段的所有环境均已经基本就绪
此时 Tomcat 相关的服务就会开始启动了
- step 10/11 : 上下文结束后处理 afterRefresh、发布上下文
这一步骤,就是应用启动阶段的最后一步,到这一步骤的时候,上下文已经被刷新、所有的 Bean 也已经被 bean 工厂生产完毕并写入进上下文,上下文相关的操作已经到尾声,接下来就是收尾工作,即上下文后处理、停止计时器、停止监听器的相关操作,处理完这些工作后就会正式发布上下文(下方流程图的 step 10、step 11)
- step 12/13 : 执行 Runner 运行器、上下文就绪并返回
接下来程序会调用 Runner() 运行器,并且发布应用上下文就绪的信号,然后返回
至此,正式启动阶段 结束(下方流程图的 step 12、step 13)
至此,SpringApplication 启动完成!(启动结束阶段)
run() 方法执行的大致的流程图如下所示:
5 自动装配再研究
思考:在 yaml 配置文件中,我们到底都可以配置哪些东西?虽然官网讲的很详细,但是似乎全部记住没那么容易
所以,能否可以找到一种 yaml 配置文件与自动装配的核心文件 spring.factories 之间有的某些联系?
我们打开核心配置文件 spring.factories,在里面随表找一个自动配置类(这里以 HttpEncodingAutoConfiguration为例)
进入该配置类,我们发现他有一个名为启用配置属性的注解 @EnableConfigurationProperties
该注解有一个参数,是一个名为 ServerProperties
的配置类的 class 文件
进入 ServerProperties
配置类,我们发现他有一个 @ConfigurationProperties
的注解,这个注解有点眼熟?
不正是给配置类绑定配置文件的注解吗?那这里说明这个 ServerProperties
类应该是绑定了一个名为 server
的配置文件
我们回到 yaml 文件中,输入 server.
,IDEA 会提示可以配置的选项,仔细一瞅,又很眼熟?不就是 ServerProperties
这个类的属性吗?
这是偶然吗?我们再去随机打开别的 Properties
配置类,发现都是如出一辙
这应该绝非偶然,而且我们也发现了一个规律:
-
在 SpringBoot 的核心装配文件
spring.factories
中,全是形如xxxAutoConfiguration
的自动配置类 -
在该自动配置类中,都有着一个名为
@ConfigurationProperties
的注解 -
并且该注解传入的参数均为形如
xxxProperties.class
的配置类的 class 文件 -
而且这个
xxxProperties
配置类中所有的属性,都可以在我们的配置文件yaml、properties
中一一对应的找到
到此为止,自动装配的原理精髓被我们初步研究出来了:
-
SpringBoot 启动时会加载大量的自动配置类(
xxxAutoConfiguration
) -
只要我们使用的组件已经在自动配置类
xxxProperties
中被配置,就不需要我们再去手动配置了(比如创建新的 Springboot 项目之后,什么都不需要配置就可以启动,那是因为端口号、Tomcat 什么的都已经自动配置好了) -
在给容器中自动配置类添加组件的时候,会从
Properties
类中获取某些属性,因此我们只需要在配置文件中指定这些属性的值即可
思考:自动配置类在所有的情况下都会生效吗?
- 我们去项目的配置文件中添加如下代码,开启调试功能
debug: true
- 然后启动主启动类,在打印出来的大量信息中,有两个东西引起了我们的注意:
Positive matches
以及Negative matches
,分别表示生效和未生效
-
既然有的配置类生效了,有的没有生效,那么是否生效是由什么东西控制的?
-
再次随便打开一个
xxxAutoConfiguration
的自动配置类,我们注意到每个配置类上都有形如@Conditionalxxxxx
的注解,这个注解就是用来控制配置类生效与否的,当该注解中所指定的配置类没有被我们手动以starter
的形式开启时,它就不会被加载,否则就会加载
- 综上所述,项目中自动配置类生效与否,由
@Conditionalxxxxx
形式的注解进行控制