SpringBoot 自动装配原理浅探

1 pom.xml

1.1 项目依赖管理

打开项目的配置文件 pom.xml,看起来和以前的 ssm 项目差不多,仔细观察发现,以前的依赖都是由 GAV 三部分构成

但是在 SpringBoot 中没有了 V(版本号),这是为什么?

image-20220518101343338

难道依赖没有版本号吗?显然不可能。继续往下找,发现有一个 <dependencyManagement> 的配置,看名字似乎是依赖管理器?

image-20220518101857803

点击进入 spring-boot-dependencies ,我们发现,原来 SpringBoot 将版本号都在 spring-boot-dependencies-xxx.pom 文件里统一进行管理了

image-20220518101940275

1.2 启动器

配置文件中,我们稍微仔细观察,会发现一个规律:好像所有的依赖都是以 spring-boot-starter 开头的?

image-20220518103639520

这应该不是偶然,我们去 SpringBoot [官方文档](Developing with Spring Boot) ,官方这里有对于启动器的描述:

image-20220518104133407

启动器是一个方便于我们在项目中引入依赖的集合

官方启动器的名字都是类似于 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 这两个

image-20220518110927201

2.2 @SpringBootConfiguration

该注解表明它是一个 SpringBoot 的配置,依次点进去该注解

image-20220518111606595

很清楚的可以看到,主启动类上的 @SpringBootConfiguration 注解,本质上是一个 Spring 的组件

2.3 @EnableAutoConfiguration

点进该注解,除过元注解外,有两个重要的注解

image-20220518142325950

  • 对于第一个注解,@AutoConfigurationPackage ,意思就是自动配置包,那么,它配置了什么东西?

再点进去该注解,发现它是导入了一个配置包选择器选择器,导入了什么选择器?

image-20220518142653705

再往里进入,发现 AutoConfigurationPackages.Registrar 注册了一些 bean,然后导入了一些元数据,这些元数据估计与包扫描有关,这里先不深入

image-20220518142950661

  • 对于第二个注解,@Import(AutoConfigurationImportSelector.class),它导入了一些选择器

进入 AutoConfigurationImportSelector 选择器 ,里面有一个名为 getAutoConfigurationEntry 的方法,根据名称可以知道,该方法是自动获取配置项目

方法中有一句如下所示代码,它的作用是获取候选配置列表

List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);

image-20220518143558001

那么怎么获取候选配置列表?点进去 核心方法 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,那么不出意外的话,它应该就是自动配置的核心文件了

image-20220518150242015

打开该文件,我们看看他到底都配置了什么东西?可以看到,配合了很多很多的配置,那么,为什么读取了这个文件后,他就能自动装配好?

image-20220518150607263

我们以我们熟悉的 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 就可以直接供我们使用!

image-20220518153242641

以上重要注解的原理图大致如下:

@ComponentScan、@SpringBootConfiguration

image-20220518172730932

@@EnableAutoConfiguration(核心注解)

image-20220518173105033

思考:经过上面的分析,我们已经知道,SpringBoot 会将从 META-INF/spring.factories 中读取并加载的所有配置类全部添加到一个名为 properties 的配置类中,供我们直接使用。那么,既然所有的配置类都被加载了,为什么很多都没有生效,需要我们去在 pom.xml 中导入对应的 starter 才会生效?

我们去 spring.factories 文件中,随意找一个我们没有使用的配置类,比如下面的 security.reactive.ReactiveSecurityAutoConfiguration 配置类

image-20220518170259033

可以很清楚的看到,编译器中直接报红,这意味着我们这些包并没有进行导入

再看该类上面的 @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 方法执行一共分为三个大阶段:准备启动阶段、正式启动阶段、启动结束阶段

  1. new SpringApplication()、init 加载初始化

当启动 SpringApplication 之后,首先创建了一个 SpringApplication 的实例

然后去执行 SpringApplication 的构造函数,使用构造函数进行 init() 初始化,一共做了以下四步操作:

  • 根据类路径推断应用是否为 Web 项目

  • 加载所有可用的初始化器

  • 设置所有可用的程序监听器

  • 推断并设置 main 方法的定义类

image-20220519100151841

  1. 开始执行 run() 方法

然后 run() 方法开始准备执行,他首先会实例化一个监听器,这个监听器在启动之后会持续监听应用程序上下文

image-20220519101343404

  1. step1 : headless 系统属性设置

该阶段中,程序开始设置 headless 相关的系统属性(下方流程图的 step 1)

image-20220519112414963

  1. step 2 : 初始化监听器 getRunListener(args)

程序将前面实例化的监听器进行初始化设置(下方流程图的 step 2)

image-20220519112529644

然后会使用 SpringFactoriesInstances 来根据传入的对象名来得到所需的工厂对象

这里的对象名,就是从 2.2 中提到的 spring-boot-autoconfigure-xxxx.jar 这个 jar 包下的 /META-INF/spring.factories 文件中所获取的

这个文件中配置了所有的自动配置对象的全限定名,工厂对象会根据该对象的 class 文件,使用反射机制得到该对象的构造犯法,最后生成一个工厂的实例对象并返回

image-20220519113436666

  1. step 3 : 启动准备好的监听器

然后将初始化完成的监听器正式启动

这个监听器会持续监听上下文,直到上下文发布完成并返回之后,它才会停止监听(下方流程图的 step 3)

image-20220519112736206

  1. step 4 : DefaultApplicationArguments

开始装配环境参数,创建了 web/standard 环境、 加载了属性源、加载了预监听集合

到此步骤为止,应用的 准备启动阶段 已经完成!(下方流程图的 step 4)

image-20220519102109234

image-20220519102514132

image-20220519102932740

  1. step 5 : 打印 banner 图案

这一步开始,应用正式开始启动,首先会打印 banner 图案(下方流程图的 step 5)

image-20220519103410075

image-20220519103522682

  1. step 6/6.1 : 上下文区域、根据类型创建上下文

到了这里就开始创建上下文区域

程序会根据 web/standard 的类型来创建与之对应的上下文(下方流程图的 step 6、step 6.1)

image-20220519103721655

  1. step 7 : 准备上下文异常报告

这一步骤中,程序会根据 SpringFactoriesInstances 来创建对应的上下文异常报告(下方流程图的 step 7)

image-20220519104055593

  1. step 8 : 上下文前置处理 prepareContext

该步骤会对上下文进行前置处理,包括监听器的配置、相关环境配置、初始化器设置、资源加载等操作

至此,上下文的前置准备工作结束(下方流程图的 step 8)

image-20220519104550201

  1. step 9 : 上下文刷新 refreshContext

step 8 中上下文初始化完成之后,接下来就是给上下文中写入东西(刷新上下文)

在该步骤中,程序会加载 bean 工厂、生产所有的 bean、完成 bean 工厂的初始化,最后再次刷新生命周期(下方流程图的 step 9)

image-20220519105931555

关于上下文的所有操作结束以后,程序启动阶段的所有环境均已经基本就绪

此时 Tomcat 相关的服务就会开始启动了

image-20220519110648027

  1. step 10/11 : 上下文结束后处理 afterRefresh、发布上下文

这一步骤,就是应用启动阶段的最后一步,到这一步骤的时候,上下文已经被刷新、所有的 Bean 也已经被 bean 工厂生产完毕并写入进上下文,上下文相关的操作已经到尾声,接下来就是收尾工作,即上下文后处理、停止计时器、停止监听器的相关操作,处理完这些工作后就会正式发布上下文(下方流程图的 step 10、step 11)

image-20220519110822801

  1. step 12/13 : 执行 Runner 运行器、上下文就绪并返回

接下来程序会调用 Runner() 运行器,并且发布应用上下文就绪的信号,然后返回

至此,正式启动阶段 结束(下方流程图的 step 12、step 13)

image-20220519111300804

image-20220519111402975

至此,SpringApplication 启动完成!(启动结束阶段

run() 方法执行的大致的流程图如下所示:

5 自动装配再研究

思考:在 yaml 配置文件中,我们到底都可以配置哪些东西?虽然官网讲的很详细,但是似乎全部记住没那么容易

所以,能否可以找到一种 yaml 配置文件与自动装配的核心文件 spring.factories 之间有的某些联系?

我们打开核心配置文件 spring.factories,在里面随表找一个自动配置类(这里以 HttpEncodingAutoConfiguration为例)

进入该配置类,我们发现他有一个名为启用配置属性的注解 @EnableConfigurationProperties

image-20220520210939501

该注解有一个参数,是一个名为 ServerProperties 的配置类的 class 文件

进入 ServerProperties 配置类,我们发现他有一个 @ConfigurationProperties 的注解,这个注解有点眼熟?

不正是给配置类绑定配置文件的注解吗?那这里说明这个 ServerProperties 类应该是绑定了一个名为 server 的配置文件

image-20220520211444304

我们回到 yaml 文件中,输入 server. ,IDEA 会提示可以配置的选项,仔细一瞅,又很眼熟?不就是 ServerProperties 这个类的属性吗?

image-20220520214506350

这是偶然吗?我们再去随机打开别的 Properties 配置类,发现都是如出一辙

image-20220520212334285

image-20220520213044563

这应该绝非偶然,而且我们也发现了一个规律:

  • 在 SpringBoot 的核心装配文件 spring.factories 中,全是形如 xxxAutoConfiguration 的自动配置类

  • 在该自动配置类中,都有着一个名为 @ConfigurationProperties 的注解

  • 并且该注解传入的参数均为形如 xxxProperties.class 的配置类的 class 文件

  • 而且这个 xxxProperties 配置类中所有的属性,都可以在我们的配置文件 yaml、properties 中一一对应的找到

到此为止,自动装配的原理精髓被我们初步研究出来了:

  • SpringBoot 启动时会加载大量的自动配置类(xxxAutoConfiguration

  • 只要我们使用的组件已经在自动配置类 xxxProperties 中被配置,就不需要我们再去手动配置了(比如创建新的 Springboot 项目之后,什么都不需要配置就可以启动,那是因为端口号、Tomcat 什么的都已经自动配置好了)

  • 在给容器中自动配置类添加组件的时候,会从 Properties 类中获取某些属性,因此我们只需要在配置文件中指定这些属性的值即可

思考:自动配置类在所有的情况下都会生效吗?

  1. 我们去项目的配置文件中添加如下代码,开启调试功能
debug: true
  1. 然后启动主启动类,在打印出来的大量信息中,有两个东西引起了我们的注意:Positive matches 以及 Negative matches ,分别表示生效和未生效

image-20220520220145760

image-20220520220122639

  1. 既然有的配置类生效了,有的没有生效,那么是否生效是由什么东西控制的?

  2. 再次随便打开一个 xxxAutoConfiguration 的自动配置类,我们注意到每个配置类上都有形如 @Conditionalxxxxx 的注解,这个注解就是用来控制配置类生效与否的,当该注解中所指定的配置类没有被我们手动以 starter 的形式开启时,它就不会被加载,否则就会加载

image-20220520220612138

  1. 综上所述,项目中自动配置类生效与否,由 @Conditionalxxxxx 形式的注解进行控制
posted @ 2022-05-19 12:14  悟道九霄  阅读(166)  评论(0编辑  收藏  举报