引言
最近几年随着微服务的发展,SpringCloud火了,随之SpringBoot这个“脚手架”逐渐被大家所喜爱,主要是它的方便和快捷,无需各种xml配置,一个主方法直接跑一个后台程序,想整合其他框架时,只需要在pom文件中引入一个starter,类上加一个@EnableXXX的注解,就可以很快的集成其他框架。
那它是怎么实现的呢?对于一个神奇而又优秀的框架,我总喜欢一层一层的扒它的思想,逐渐解析其原理,今天我们主要先看一下自动装配的相关思想和实现。
自动装配思想浅析
从上图我们可以知道,SpringBoot其实就是帮我们导入了一些类,比如SpringMVC、Mybatis、Hibernate的一系列类定义,在启动的时候根据引入的starter来判断是否需要注入相关的Bean到容器
抛开SpringBoot已经实现的技术,已知Spring提供了很多后置处理器,根据这些已知的思想,假如SpringBoot如果是由我们来实现呢?
1. 已知需求:我们需要根据用户引入的jar包,批量帮其注册一批配置类?
(这里估计很多人都会说,SpringBoot程序不就是一个main函数,为什么要先从注解讲起来?其实从main函数讲起来也没毛病,但是那是服务启动的原理,放到源码解析(下)中讲,这里先跳过main函数解析的过程,假设main函数就是创建了一个Bean容器和内嵌的tomcat,随后我们直接跳到我们需要理解的自动装配;并且我们假设这里读者已经熟悉了Spring Bean加载的流程,如何解析Configuration的过程我们在Spring源码专题已经逐一解析)
回到Spring的解析配置中,后置处理器ConfigurationClassPostProcessor它主要在processConfigBeanDefinitions方法中从我们的根配置类(main函数的启动类)逐层扫描所有的配置类,它会按照以下的流程顺序逐渐解析注解中相关的Bean定义:
(注:图中为解析(装成BeanDefinition)顺序,非加载(解析BeanDefinition注册成Bean)顺序)
所以,我们根据上面的注解,可以考虑(传入配置类的方式优先淘汰,用户传入复杂度更高):
- @ComponentScan注解根据包扫描Bean定义,传入指定的包名,缺点无法掌握扫描顺序,且包名不能重复 (可以实现)
- @Import注解支持三种类型的Bean导入
- 普通的Configuration
- 支持批量导入的ImportSelector
- 支持动态注册Bean定义的ImportBeanDefinitionRegistrar
可知Import注解的功能是非常强大的,我们优先考虑这种,缺点仍然是无法掌握加载顺序 (最佳实现)
- @Bean注解支持方法注册Bean定义,缺点硬编码无法动态配置 (最次实现)
- @ImportResource注解支持导入xml文件,缺点又回到到了Spring配置XML时代 (较次实现)
我们这里根据已知的注解实现,首先选择Import注解,导入我们需要的类,对于“需要”是根据我们已知的pom文件中的starter,其实就是根据一定的条件注入这些配置类,于是就有新的问题:如何根据条件导入配置类呢?
2. 已知需求:我们需要根据当前引入的Jar包,再判断是否引入相关配置类?
嗯,针对这个方向,我们首先想到的就是判断是否能够加载某个框架的核心类(此时可以联想到Spring4为什么要引入条件注解了吧),然后再根据条件导入某项特定的配置类,于是思路可以如下:
主要需要判断的条件如下,右边为对应Spring4引入的条件注解:
- 判断是否引入框架核心类:根据AppClassLoader判断是否引入相关框架的核心类(因为引入POM依赖后所有的Jar都会被AppClassLoader加载)————@ConditionalOnClass:类加载器已经加载过某类
- 判断是否存在用户的核心配置:根据BeanFactory判断是否已经扫描到用户配置的核心类(这一点依赖Bean的扫描顺序,直接判断有没有问题呢?)————@ConditionalOnBean、@ConditionalOnMissingBean:当容器中存在或者不存在时才实例化
- 判断是否处于Web容器:和1判断类似,使用AppClassLoader判断是否引入相关容器————@ConditionalOnWebApplication:项目是一个Web项目时进行实例化
- 用户自定义的条件...————@ConditionalOnExpression根据表达式计算为true时注入容器、@ConditionalOnProperty指定的属性有指定的值时进行实例化
对于筛选条件,我们已经解决了,那么关于上述第二点,@ConditionalOnBean和@ConditionalOnMissingBean依赖Bean加载的顺序,如果不实现顺序加载,就会出现以下问题:
之前我们了解到Spring对Bean的扫描是没有顺序可言的,顺序解释如下:
- Import导入相关核心包,需要判断@ConditionalOnMissingBean当没有配置某项Bean的时候使用自动装配的配置(比如@ConditionalOnMissingBean(CacheManager.class),判断用户未配置CacheManager时给定系统默认的CacheManager)
- 此时需要判断beanDefinition(Spring容器中的Bean定义是否存在),此时会根据扫描的顺序出现两种情况
- 之前未优先扫描用户包,判断配置类不存在装入容器,之后扫描到用户类出现重复定义异常(异常情况)
- 之前优先扫描过用户包,判断已经加载了指定的配置类,之后只会注入用户定义的配置类到容器(正常情况)
我们之前也说到,Spring没有定义顺序加载机制,如果默认情况则是按照文件系统的排序规则,Jar包的顺序;这些规则在不同操作系统上也不斤相同,所以是不可靠的,那么新的问题来了:
3. 已知需求:我们需要将判断逻辑的类延迟加载,用户定义的类全部在之前加载?
针对这一种场景,Spring4在ImportSelector新增了一个实现:DeferredImportSelector延迟加载的ImportSelector
- 延迟加载所有导入的实现类
- 自定义返回加载的顺序
这个流程在Spring源码解析中已经讲到过(可以翻阅以前文章进行复习),DeferredImportSelector主要实现的功能为让所有ImportSelector导入的类可以自定义加载顺序,并且这些类是在加载完成用户的@Bean、@Component、@Configuration注解之后的加载的
至此,我们自定义实现自动装配的逻辑已经完成了,实际上SpringBoot的核心思想就是如此,不过具体的实现逻辑要复杂很多,因为可能需要考虑各种场景,当然那是需要在实践细节中巩固了。
SpringBoot实现自动装配流程图
这里我们再看一下SpringBoot中实现的流程,可以先从总的流程来看,避免上来就过度陷入细节。SpringBoot的主程序从加上@SpringBootApplication注解,到将starter的类注入到容器,主要经历了以下结果流程。(边边角角的截图已经省略了,文末有processOn的链接可自取)
代码流程中,主要分为以下几步:
-
@EnableAutoConfiguration注解 嵌套 @Import(AutoConfigurationImportSelector.class),而Import导入的这个类AutoConfigurationImportSelector实现了selectImports方法
-
AutoConfigurationImportSelector类是自动装配的核心类,它还实现了DeferredImportSelector这个接口,简单来说它就是:
- 延后加载的ImportSelector
- 支持自定义的加载Bean顺序规则(这是一个比较重大的突破,在之前Bean的加载顺序完全取决于扫描包的顺序,也就是文件系统的本身顺序)
(注:在此之前有@Order注解,它解决的问题是执行顺序,和加载顺序无关)
DeferredImportSelector 提供了接口方法:getImportGroup(),返回需要自定义排序的规则Group类
(如果实现了该接口方法,返回了非空的Group,那么就不会再调用ImportSelector中的selectImports方法)
-
AutoConfigurationImportSelector实现了DeferredImportSelector的getImportGroup()方法,返回AutoConfigurationGroup.class实例
-
AutoConfigurationGroup中定义了process方法和selectImports方法,首先process会加载所有在jar包文件夹中META-INF/spring.factories配置数据,然后调用Group实现的selectImports方法时再根据指定的@Order、@AutoConfigureBefore、 @AutoConfigureAfter规则排序,返回排序后的BeanName信息逐一注入Spring容器
总的来说核心流程可以看下图:
解析源码的原理,并不是说直接看懂每一个方法就完了,这样只是可以学会一些写代码的技巧和作者的整体思路,我们还可以在作者的角度思考,逐一参透其奥秘,比如:为什么用ImportSelector不行,为什么要延迟的DeferredImportSelector之类等。
小节
SpringBoot的自动装配实际上类似于对所有框架进行做了一个抽象组件工厂,这些组件的核心配置类都会加载进来,因为组件并不是都是全部需要的,引入了条件注解筛选这部分需要的组件,最后条件注解也会引入新的问题:条件一定要最后判断,于是就有了DeferredImportSelector的实现。
Springboot的自动装配原理到此就结束,相信看了这篇实现的思想,自己也能自定义实现一个自动装配组件,代码的细节这里就在文中指出了相关核心类。下一篇将继续总结SpringBoot的启动原理部分