springboot源码学习。(SPI和自动装配)

起因

事情的起因是这样的题主最近在研究spring amQP 的源码

有个博主写的特别好

Spring AMQP源码解析-CSDN博客

我在这里面其实看到不少熟悉的知识点。

这里就是spring自动装配的流程,其完整流程就是SpringBoot启动->@SpringBootApplication->@EnableAutoConfiguration->AutoConfigurationImportSelector扫描META-INF/spring.factories->加载RabbitAutoConfiguration->创建RabbitMQ相关Bean

RabbitProperties类加了@ConfigurationProperties会去读取配置文件中的参数,否则就提供类属性里面的默认配置,RabbitAutoConfiguration用@Bean注解通过方法将RabbitTemplate,CachingConnectionFactory都会注册成bean,在方法里面会注入RabbitProperties的bean给他们设置参数。

过去我一直没有理解的自动装配和spi相关的机制都在研究Spring AMQP的时候串联起来了。

我们看到想springboot其实是有维护自己的mq包的就是

org.springframework.boot.autoconfigure这个

有了这个包所有像amqp这样的starter才能够被被读到。也就是说当我们自定义starter的时候。由于没有这个自动装配包的预配置所以必须自己走一编这个包做的事情。才能够实现如一些类似的配置读取啊这样的操作。然后这个包(org.springframework.boot.autoconfigure)是如何被读的?理解这个过程也就可以理解到如果我想要覆盖某些默认的自动配置策略。以及认为的去发现starter。这个就是所谓的springboot的spi机制。

我们都知道自动装配机制是围绕这个机制@EnableAutoConfiguration-去展开的吗?

我们不妨看看这个注解是怎么被扫描的。

这个注解我记得追踪到底就是一个引入了一个selector选择器特定的import。

我们先来看下整个main方法是怎么启动的? (主流程和contenxt阶段)

我们这里就能够直接看到一个老熟人。

SpringBootApplication 注解.......以及一个run方法。

@SpringBootApplication 的定义

@SpringBootApplication 是一个 组合注解 ,本质上等价于:

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@Configuration@ComponentScan@EnableAutoConfigurationpublic @interface SpringBootApplication { }

所以,它实际上做了三件事情:

  1. @Configuration
    1. 标记当前类是一个配置类,可以包含 @Bean 方法。
  2. @ComponentScan
    1. 会扫描当前包及子包下的 @Component、@Service、@Repository 等 Bean。
    2. 这里扫描的是 com.example.zuoye 包及其子包。
  3. @EnableAutoConfiguration
    1. 启用 Spring Boot 自动装配机制。
    2. 核心作用是扫描 spring.factories,按条件加载自动配置类。

main 方法里 SpringApplication.run()

SpringApplication.run(zuoyeApplication.class, args);

  • 参数 zuoyeApplication.class 就是你的 @SpringBootApplication 类。
  • Spring Boot 会读取这个类上的注解,触发以下流程:

创建 SpringApplication 对象

SpringApplication application = new SpringApplication(zuoyeApplication.class);

  • SpringApplication 会记录你的 main class,以及监听器、环境变量等。

执行 run() 方法

run() 里会:

  1. 创建并准备 Environment
  2. 创建 ApplicationContext
  3. 执行 ApplicationContextInitializer
  4. 注册 BeanDefinition
    1. 扫描 @Configuration 类(你的 main 类)
    2. 执行 @ComponentScan → 扫描 DAO、Service、Controller 等
    3. 执行 @EnableAutoConfiguration → 调用 AutoConfigurationImportSelector → 读取 spring.factories → 条件加载自动配置类
  5. 刷新 ApplicationContext → 实例化 Bean、依赖注入
  6. 启动完成,发布 ContextRefreshedEvent

这两张图是截至到从启动到预加载stater前的栈。从下到上看。

整个的调用栈并不算复杂。

我们关注几个关键的节点

在启动之后的第一个逻辑步骤包含着一系列分支

main() -> SpringApplication.run()

├─> 初始化监听器 listeners.starting()

├─> 创建 Environment(读取配置文件)

├─> 打印 Banner

├─> createApplicationContext()

├─> prepareContext() → 扫描包 & 自动装配 (@SpringBootApplication)

├─> refreshContext() → Bean 实例化 + 依赖注入

├─> afterRefresh() & log startup info

├─> listeners.started()

└─> callRunners()

我们追入到这个context看看怎么加载的。

在这个 prepareContext() 有一个这初始化方法/。

这个方法也很简单就是遍历所有初始化器使用。

那么有那些初始化器呢?

如果是 Spring Boot 默认启动,你会发现 getInitializers() 返回的集合通常包含:

|----------------------------------------------|-------------------------------------------------------------|
| 初始化器 | 功能 |
| ConfigDataApplicationContextInitializer(新版本) | 加载 application.properties / application.yml 配置文件,处理 profile |
| ContextIdApplicationContextInitializer | 设置 ApplicationContext 的 ID |
| ParentContextApplicationContextInitializer | 注入父上下文 |

题主这里的初始化器就这个?

但是题主的boot是2.2.7的通过查阅资料发现老板本的是没有ConfigDataApplicationContextInitializer(新版本)这个的。而这个初始化器的职责则是由这个负责

|------------------------------------|-----------|
| (老版本)ConfigFileApplicationListener | 旧版本处理配置文件 |

而listener的调用就在初始化器下方

也是一个遍历方法。

我们插播一个问题即这些Linstener和初始化器有是什么时候加载进来的呢?

我们结合debug深入研究下spring是如何扫包 和依赖的(读文件的) 的? Environment 阶段

通过断点我们知道了是发生在context之前的。

注意传给下游一个SpringApplicationRunListener.class

这是一个实际发生类加载器相关的代码

我们直接追到最底层。

关注类加载行为中写入的这个几个方法和常量。

ClassLoader.getResources(String name)

从指定 ClassLoader 的 classpath 中查找所有匹配 name 的资源

ClassLoader.getSystemResources(String name)

系统类加载器 (通常是应用程序的主 ClassLoader)查找资源

这里是一个写死的路径和写法。

我们不禁拷问凭什么类加载可以传这样的文件格式,就能类加载。

系统类加载器 (通常是应用程序的主 ClassLoader)查找资源。

原因在于

spring.factories 本质上是一个 标准的 Java Properties 文件

而类加载器本质上是支持Properties 文件的。

也就是说我如果弄了一个同名Properties 替换。是不是在整个spring初始化中吧奇奇怪怪的东西给加载进去了呢?

值得注意的是

不管是getResource(), 还是getSystemResources(), 这些方法都仅仅是资源的定位方法, 终于都仅仅是返回一个URL, 仅仅是对资源的定位而已, 我们随后应通过自己的方法来读取这些资源. 而在Class和ClassLoader中还定义的有getResourceAsStream方法, 该方法是getResource的增强版, 这里就不介绍了.

补充双亲委派的体现。 (题外话)

类加载在代码中的体现是链表结构。每个类加载的类都有一个parent。指向自己的父加载器

而双亲委派的真面目是这样的.......也就是说双亲委派是可以讨论多种方式实现的(体现在父子关系赋值的复杂性和可变性)。 我曾看到过同事自定义的类加载器继承某个类加载器。然后通过在构造器中super父类的类加载器实现另类的双亲委派。而破坏双亲委派也很简单就是重写这个loadclass方法..........而最常规的写法则是在外部调用中直接往构造器中传指定parent......

也就是说我们考虑一种可能性。就是在类加载到一半的时候写个方法直接中断这个继承链表接到另外一条链表上就可以实现很多危险也让人兴奋的操作。

(spring如何加载配置文件的?数据库连接这种) ConfigFileApplicationListener的 类加载( Environment 阶段

这里就不展开说了。我们还是吧重点放在我们的ConfigFileApplicationListener

的类加载中。

我们通过对listener的类加载器打断点我们可以发现。 ConfigFileApplicationListener

这个类加载器的调用栈是发生在这个环境阶段。

ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);

我们关注这个跳转。

eventListener就是一个顶级的Listener....。这样我们验证了这个所谓事件发布流程......

这是一段相当奇怪的写法因为往链表中加入null是有效的。所以当其他环境相关的配置文件不存在的时候就会去加载默认。大小为1。而一旦不等于1就按照环境加载。

也就是说回到我们的代码分支中。我们会得出一个很奇怪的结论就是第一个处理的元素是null。

我们看下这个null传参下去做什么处理

代码是这么走的

我们可以看到

第一次

第二次

通过打断点我们知道整个路径的传递过程。是不断拼接和尝试读取默认路径的过程......

这个递增的过程就是靠每次递归的location+name。然后根的切换依赖于上游的

那当我们使用不同环境的时候会怎么加载呢?

我们看这个方法就是在这里控制的。

总结就是

application.properties 始终会加载(因为 null是第一个头节点)。

如果没写 spring.profiles.active,Spring Boot 会兜底启用 "default"。

只要你显式配置了 spring.profiles.active,就不会再额外加 "default"

就先说到这里。其实往深的说应该还要把微服务场景下。如何从数据库或者像Nacos这样的一些平台去拉配置这边就先略过了。

补充关于spring.profile.active, 环境配置相关的说明

SpringBoot配置文件中spring.profiles.active配置详解-CSDN博客

spring关于listener的事件机制(context阶段)

我们回到主线。我们已经知道了在做context的时候会扫描到我们的listener。

然后通过一系列反射调用去构造我们的对象。

在这个过程中完全了实例构造化。但是我们注意到这种通过反射拿到的构造器方法

是有条件的也就是并非所有listener都会在这个时候实例化

这个调用 (SpringApplication, String[]) 构造器 。这个加载部分的实例类

我们不禁问其他的listner什么时候实例化?

这其中涉及到一个复杂的事件机制。

所以是在classload.getResource完成了一次listener的路径扫描。但是我们要注意不是所有listener都是一视同仁的只有最顶级的linstener能够在启动流程中同步的实例化。其他的listener都是被这个主listener通过事件异步的去初始化。

包括我们可以看到在spring.factories文件中都吧他们描述为不同的listener。

contenxt和Environment

我们知道prepareContext 里面有一个初始化方法也是和初始化器和listener相关的。为什么要在contenxt遍历初始化这些初始化器。我这里其实用词不太恰当。因为刚刚我们已经通过代码等多种方式已经明确了一个事实就是这些初始化器和listener的生命周期并非全部是由context触发和管理的。所以这里的初始化更加有点类似于装载的概念。那我们就很容易问为什么要区分这两个对象。一步到位不就行了吗?

这个主要是spring的 解耦 理念

  • Environment 负责 外部化配置 (属性、配置文件、系统环境)。
  • ApplicationContextInitializer 负责 内部容器定制 (BeanFactory、Profile、额外的 Bean 定义)。
  • 这样分层之后,Listener → Environment → Initializer → Context → Refresh 形成一条清晰的链路。

我们通过代码也可以知道这个步骤是context吧自己传进去。Spring Boot 并不是将初始化器和监听器直接交给 ApplicationContext 来管理,而是通过 SpringApplication 来进行管理。画一个大致的图就是

其实我个人的理解还是说Environment是一个类似于启动编译态。context就像是类似于运行态一样的概念。

但是我们发现当本地没有factories文件时我们可以观察到这个断点是走不到的。通过更仔细的日志级别。我发现了依赖在上游初始化就已经被扫描了。所以我们推测这个初始化器读的就只是本地的spirng.factories而依赖注入和自动装配应该是优于这个步骤并且应该是交由其他类负责的。

SpringApplicationRunListeners listeners = getRunListeners(args); 这个方法才是调用了类加载器的方法去扫描具体的路径 。只不过日志是在环境这边出来。配置文件的如数据库这些的加载是在ConfigFileApplicationListener则是在环境阶段二次扫描。然后context实例化listener也是实例那个事件相关的listener。

refreshContext阶段

最重要的一跳路线是

从 SpringApplication.run() 进去,关键路径是:

  1. refreshContext(context)

→ 实际调用的是 AbstractApplicationContext.refresh()。

  1. 在 refresh() 里:
    1. 执行 invokeBeanFactoryPostProcessors(beanFactory)。
  2. 这里会调用 ConfigurationClassPostProcessor,这是 Spring 处理 @Configuration 和 @Import 的核心处理器。
  3. @EnableAutoConfiguration
  4. 本质是一个 @Import(AutoConfigurationImportSelector.class)。
    1.

所以 ConfigurationClassPostProcessor 会识别到它。

然后执行 AutoConfigurationImportSelector#selectImports。

这个方法里调用

SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, classLoader)。

    1. 这一步把 spring.factories 中 EnableAutoConfiguration 的所有配置类读出来。

这个就是就下面的refersh

复制代码
public` `void` `refresh()` `throws` `BeansException,` `IllegalStateException` `{`
		`synchronized` `(this.startupShutdownMonitor)` `{`
			`// 为刷新准备上下文`
			`prepareRefresh();`

			`// 告诉子类刷新内部bean工厂。`
			`ConfigurableListableBeanFactory beanFactory =` `obtainFreshBeanFactory();`

			`// 准备bean工厂以便在此上下文中使用。`
			`prepareBeanFactory(beanFactory);`

			`try` `{`
				`// 允许在上下文子类中对bean工厂进行后处理				postProcessBeanFactory(beanFactory);`

				`// 调用在上下文中作为bean注册的工厂处理器。`
				`invokeBeanFactoryPostProcessors(beanFactory);`

				`// 注册拦截bean创建的bean处理器。`
				`registerBeanPostProcessors(beanFactory);`

				`// 为此上下文初始化消息源。				initMessageSource();`

				`// 			为此上下文初始化事件组播器。`
	`initApplicationEventMulticaster();`

				`// 初始化特定上下文子类中的其他特殊bean。`

				`onRefresh();`

				`// 检查侦听器bean并注册它们。`

				`registerListeners();`

				`// 实例化所有剩余的(非lazy-init)单例。`
				`finishBeanFactoryInitialization(beanFactory);`

				`// 最后一步:发布相应的事件`
				`finishRefresh();`
			`}`

			`catch` `(BeansException ex)` `{`
				`if` `(logger.isWarnEnabled())` `{`
`					logger.warn("Exception encountered during context initialization - "` `+`
							`"cancelling refresh attempt: "` `+ ex);`
				`}`

				`// Destroy already created singletons to avoid dangling resources.`
				`destroyBeans();`

				`// Reset 'active' flag.`
				`cancelRefresh(ex);`

				`// Propagate exception to caller.`
				`throw ex;`
			`}`

			`finally` `{`
				`// Reset common introspection caches in Spring's core, since we`
				`// might not ever need metadata for singleton beans anymore...`
				`resetCommonCaches();`
			`}`
		`}`
	`}`

`

invokeBeanFactoryPostProcessors我们关注这个

注意这条注释

// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)

// (例如,通过ConfigurationClassPostProcessor注册的@Bean方法)

看下函数栈帧调用从这个入口进去

进入到这个委派类一个经典的接口写法模板方法加观察者的写法。

我们的ConfigurationClassPostProcessor实现了这个接口就被调度到了

我们关注这个方法就是加载的配置类的

读取注解是一个递归的过程这里还涉及到一些异步技术和延迟加载。

总体流程是这样的

SpringApplication.run()

→ refreshContext()

→ refresh(ApplicationContext)

→ AbstractApplicationContext.refresh()

→ invokeBeanFactoryPostProcessors()

→ ConfigurationClassPostProcessor

→ 处理 @EnableAutoConfiguration

→ AutoConfigurationImportSelector.selectImports()

→ SpringFactoriesLoader.loadFactoryNames()

→ 读取所有依赖 jar 包里的 META-INF/spring.factories

可以通过打断点验证。

相关推荐
知识分享小能手2 小时前
React学习教程,从入门到精通,React 单元测试:语法知识点及使用方法详解(30)
前端·javascript·vue.js·学习·react.js·单元测试·前端框架
微笑尅乐4 小时前
力扣350.两个数组的交集II
java·算法·leetcode·动态规划
rzjslSe4 小时前
【JavaGuide学习笔记】理解并发(Concurrency)与并行(Parallelism)的区别
java·笔记·学习
Cherry Zack5 小时前
了解Django模型,从基础到实战
python·学习·django
青柠编程5 小时前
基于Spring Boot的竞赛管理系统架构设计
java·spring boot·后端
茯苓gao5 小时前
CAN总线学习(四)错误处理 STM32CAN外设一
网络·笔记·stm32·单片机·学习
꒰ঌ 安卓开发໒꒱5 小时前
Java面试-并发面试(二)
java·开发语言·面试
Mr.Aholic5 小时前
Java系列知识之 ~ Spring 与 Spring Boot 常用注解对比说明
java·spring boot·spring
虚行6 小时前
C#上位机 通过ProfitNet连接西门子PLC教程--系统模拟环境搭建
学习·c#·plc
月夕·花晨6 小时前
Gateway-断言
java·开发语言·分布式·spring cloud·微服务·nacos·sentinel