springboot2.x 源码笔记-Bean的定义BeanDefinition invokeBeanFactoryPostProcessors

springboot的源码(spring)主要分为几个部分

1、构造SpringApplication,完成spring.factories文件中Initializers与Listeners的加载
2、加载配置文件,通过ConfigFileApplicationListener
3、加载BeanDefinitionRegistryPostProcessor与BeanFactoryPostProcessor完成bean的定义包装(非生成实例)
4、生成bean实例以及初始化

本文主要针对第三点,解读bean的定义过程,该部分只是完成xml配置,或者注解的bean的相关属性解析,并没有生成相应的bean实例,后续第四点中将根据bean的定义获取实例。

基于springboot2.1.4

项目概况如图:

项目地址:https://gitee.com/eshin/springbootdemo.git#autotest

  • AutotestApplication为主类
  • HelloController、HelloService为普通的注解类,HelloController注解@Import(HelloConfiguration3.class)
  • HelloConfiguration是@Configuration标注的类,同时含有非静态内部类HelloConfigurationInner1和静态内部类HelloConfigurationInner10,配置在spring.factories中,包名与启动类不一致,不会被扫描到
  • HelloConfiguration2是@Configuration标注的类,同时含有非静态内部类HelloConfigurationInner2和静态内部类HelloConfigurationInner20,并注解@Import(HelloConfiguration.class)
  • HelloConfiguration3是@Configuration标注的类,同时含有非静态内部类HelloConfigurationInner3和静态内部类HelloConfigurationInner30

    由于debug过程中有大量的bean的定义,设置断点的条件只debug自己代码中的bean的定义

    部分参考如下:
  • eg1:(beanName != null) && (beanName.contains("autotestApplication") || beanName.contains("helloController") || beanName.contains("helloConfiguration") || beanName.contains("order")) || beanName.contains("getStr"))
  • eg2:(configClass.getBeanName() != null) && (configClass.getBeanName().contains("autotestApplication") || configClass.getBeanName().contains("helloController") || configClass.getBeanName().contains("helloConfiguration") || configClass.getBeanName().contains("order")) || configClass.getBeanName().contains("getStr"))

启动执行线索:com.eshin.autotest.AutotestApplication#main---》org.springframework.boot.SpringApplication#run(java.lang.Class<?>, java.lang.String...)---》org.springframework.boot.SpringApplication#run(java.lang.String...)

一、创建上下文对象createApplicationContext();

在第一点构造SpringApplication,会通过org.springframework.boot.WebApplicationType#deduceFromClasspath设置应用类型
本次debug应用引入spring-boot-starter-web包,会被定义设置成WebApplicationType.SERVLET类型的应用,context为AnnotationConfigServletWebServerApplicationContext,构造改context对象时,主要生成AnnotatedBeanDefinitionReader和ClassPathBeanDefinitionScanner实例,同时创建DefaultListableBeanFactory实例

  • 1.1 AnnotatedBeanDefinitionReader
  • 1.2、ClassPathBeanDefinitionScanner

org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan,后续将使用该类的改方法进行扫描被注解的类和方法,解析成bean的定义注册到上下文中


二、prepareContext()

org.springframework.boot.SpringApplication#prepareContext---》org.springframework.boot.SpringApplication#load--》org.springframework.boot.BeanDefinitionLoader#load(java.lang.Object)


三、refreshContext()

org.springframework.context.support.AbstractApplicationContext#refresh
本文只关注与bean的定义相关的地方invokeBeanFactoryPostProcessors(beanFactory);

debug进入这个方法,直到org.springframework.context.support.PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors()
该方法会遍历1.1中已经定义的processor执行对应的postProcessBeanDefinitionRegistry方法,本文只关注org.springframework.context.annotation.ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry

--》org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions

  • 3.1 对configclass(各种注解生成bean的class)的解析

首个beanName autotestApplication ,既是main方法的classname
该bean的定义在二、中已完成定义,它是整个定义bean的过程的源头,解析configurationclass从它开始


进入parser.parse(candidates);

org.springframework.context.annotation.ConfigurationClassParser#parse(org.springframework.core.type.AnnotationMetadata, java.lang.String)---》org.springframework.context.annotation.ConfigurationClassParser#processConfigurationClass

图中1部分先放着,后面会讲到,先看2 doProcessConfigurationClass(configClass, sourceClass);
先整体看下这个方法

当某个配置类没有内部配置类、@ComponentScan注解,@Import注解、@ImportResource,那这个配置类在到执行该方法时是笔直通过的
、但当某个配置类含有内部配置类(eg:HelloConfigurationInner2)、有@ComponentScan注解(eg:AutotestApplication:@SpringBootApplication包含@ComponentScan)、有@import (eg:HelloConfiguration2、@EnableAutoConfiguration中的@Import(AutoConfigurationImportSelector.class))、有 @ImportResource,当存在这几种情况,便开始org.springframework.context.annotation.ConfigurationClassParser#processConfigurationClass方法的递归之路。


上面提到,首个进入该方法的configclass是autotestApplication,正是项目的启动类,在二、prepareContext()中已完成了定义注册
autotestApplication主要的注解

  • 3.1.1、那首先是对autotestApplication的@ComponentScan注解的处理
  • 3.1.1.1、条件注解处理,条件注解的实现类很多,此处仅以OnBeanCondition为例,不做详述,可尝试写自定义条件注解demo
    注意:此处的条件注解只针对@configuration注解的类,作用在@bean上的条件注解在从configclass中定义@bean注解的方法时另外处理,请看3.2

  • 3.1.1.2、扫描指定范围包并解析定义相关配置类

registerBeanDefinition最终执行到的org.springframework.beans.factory.support.DefaultListableBeanFactory#registerBeanDefinition,这个方法主要是将bean的定义放入beanDefinitionMap中,和(二、prepareContext())中对autoTestApplication定义时后面的处理是同一个地方。此时,静态内部类也会被扫描到并注册,但是非静态内部类不会被扫描到(有兴趣了解原因可以拉到最后看附录)
图中AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);会对@Scope的处理,如果ScopeProxy不是no或者default,会对其定义重新封装beanName变成下图所示

  • 3.1.1.3、遍历2中注册的bean定义

继续执行parse方法,实则就是递归org.springframework.context.annotation.ConfigurationClassParser#processConfigurationClass方法,也就是说递归栈中,前一个(先进栈)configClass的@import @importresource,@bean 都要比后一个configClass的@import @importresource,@bean,要后处理,即autotestApplication中的@import @importresource,@bean要在扫描到的configClass都处理完之后,才会处理。例如,此demo中HelloConfiguration2中的@Import(HelloConfiguration.class)处理要比EnableAutoConfiguration中的@Import(AutoConfigurationImportSelector.class)(属于configClass:autotestApplication)先处理。


在org.springframework.context.annotation.ConfigurationClassParser#processConfigurationClass的最后一行this.configurationClasses.put(configClass, configClass);
中,configurationClasses是个LinkHashMap,是有顺序的,先put进来的configClass优先被解析其中配置的bean。完整map如下图


如图:以HelloConfiguration2为例,首先先处理了她的内部类(被扫描到),其次处理他的import操作,因此HelloConfiguration2本身put进configurationClasses中的顺序比她的内部类和import的类要后
在处理configclass:HelloConfiguration2,又通过processMemberClasses()处理内部类,这个方法最终也是调用processConfigurationClass方法递归,此时,处理内部类不会删除原先的而重新put到configurationClasses,而是被标记为
importedBy HelloConfiguration2。(通过该方法处理的内部类都会被标记importedBy)
processMemberClasses中会使用到importStack用于防止循环导入,如果,在内部类中@import了某个类,这个类又@import了HelloConfiguration2,就会有循环处理。同时,在debug过程中,通过这个importStack也可以查看当前递归的深度及所处位置。


HelloConfiguration2同时使用了@Import注解,后面和autotestApplication的放一起查看源码,其他的以此类推。
同理,由于autotestApplication是递归的最开端,那么在整个scan的过程中,是最晚放入到configurationClasses中的


  • 3.1.2、处理@Import processImports()
    图中三种import类型的来源
  • @SpringBootApplication-->@EnableAutoConfiguration-->@AutoConfigurationPackage-->@Import(AutoConfigurationPackages.Registrar.class)
  • @SpringBootApplication-->@EnableAutoConfiguration-->@Import(AutoConfigurationImportSelector.class)
  • @EnableAsync-->@Import(AsyncConfigurationSelector.class)
    另外还有一种就是HelloConfiguration2中直接import的configuration类@Import(HelloConfiguration.class)分别对应不同的处理方式,主要关注
    this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector)如何加载配置在配置文件中的配置类

    此时,不管在何种情况下执行到handler.processGroupImports() 都表明,ComponentScan的部分已经处理完了。
    如上图,现在主要关注如何获取spring.factories中配置的配置类

    group.getImports()
    由此可以看出,只有当使用org.springframework.boot.autoconfigure.AutoConfigurationImportSelector对spring.factories进行加载时,才有对加载出来的configClass进行排序,而ComponentScan扫描出来的并不参与此次排序
    这也就是为何在demo的代码中配置的@AutoConfigureBefore和@AutoConfigureAfter并不会生效


    processImports()方法,之前已经解读过了,但是还有个细节没有说到,在demo中,即在HelloConfiguration2中@Import(HelloConfiguration.class),又在spring.factories中配置了HelloConfiguration,那会不会重复加载呢?答案是不会的
    processImports()-->org.springframework.context.annotation.ConfigurationClassParser#processConfigurationClass
    通过这段代码可知,当在configurationClasses中已经存在某个配置类时,如果是通过@import再次加载该类,就并不会再次加载,只是在importedBy列表中添加第二次import时使用@import的类
    如图
    但是如果demo中@import的类又被扫描到的话则扫描的会覆盖@import的,所以通过@import可以被扫描的配置类来打到改变configclass的顺序是无法实现的
    例如本demo中,HelloConfiguration3,包含静态内部类和非静态内部类,HelloConfiguration3本身和其静态内部类都是会被@ComponentScan扫描到的,而非静态内部类则不会
    那么,如果通过@import(HelloConfiguration3.class)可以改变非静态内部类在configurationclass列表中的位置,而对HelloConfiguration3本身和其静态内部类则无效

    至此,configurationClasses中就包含了所有需要解析包含bean配置的configclass了,但还缺少一个beanMethods设置的说明
    每个configclass在org.springframework.context.annotation.ConfigurationClassParser#processConfigurationClass解析的时候都会将其中用@Bean注解的方法解析添加到beanMethods列表中


    至此org.springframework.context.annotation.ConfigurationClassParser#parse(java.util.Set<org.springframework.beans.factory.config.BeanDefinitionHolder>)的解析过程就完成了。整个parse过程,就是按顺序往configurationClass列表中添加扫描到的和import的配置类。
    由此图可以,configurationClass中,通过spring.factories加载的是有排序过的,但是通过ComponentScan扫描的是没有排序的,但是有时候希望通过ComponentScan的也有排序,那可以通过内部类的配置或者import含有非静态内部类(配置类)的方法

=====================================================================================》

  • 3.2、从configClasses中loadBeanDefinitions

进入this.reader.loadBeanDefinitions(configClasses);
此时,有对@Scope的处理,如果ScopeProxy不是no或者default,会对其定义重新封装,封装后注册的definition如下图

在org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForBeanMethod方法中也有对@bean的条件注解:**
org.springframework.boot.autoconfigure.condition.OnBeanCondition#getMatchOutcome-->org.springframework.boot.autoconfigure.condition.OnBeanCondition#getMatchingBeans-->
所以bean定义的顺序对@Bean上的onBeanConditional方面的条件注解会有影响,例如,在应用代码中,想要通过spring.factories中的bean作为条件注解判断条件,这时候是会失效的,因为此时,bean的定义中还没有spring.factories中的相关bean的定义,通过spring.factories加载到的configclass,在configurationClass有序集合中是排在比较后面的。

  • 解析完configClass之后回到org.springframework.context.support.PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors()

    继续调用其他的BeanFactoryPostProcessors对已经加载的beanDefinition进行包装修改,例如org.springframework.context.annotation.ConfigurationClassPostProcessor#postProcessBeanFactory该方法会将所有的@Configuration注解的beanDefinition用enhancer包装一层代理

最开始传入的beanFactoryPostProcessor都是通过new出来的实例然后添加到org.springframework.context.support.AbstractApplicationContext#beanFactoryPostProcessors列表中的,后面的beanFactoryPostProcessor是通过从beanDefinitionNames查找到的,有各自的顺序,具体顺序代码注释有说明可参考此处


至此,bean的定义就基本完成了。
最终在org.springframework.context.support.AbstractApplicationContext#refresh-->org.springframework.context.support.AbstractApplicationContext#finishBeanFactoryInitialization--->org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons会按在beanDefinitionNames中的顺序实例化(如果有被autowired的bean定义会被提前实例化)实例化的流程将另外篇幅解读。beanDefinitionNames是一个有序列表,定义的bean的beanName按定义的顺序放入该列表


在org.springframework.context.support.AbstractApplicationContext#refresh的invokeBeanFactoryPostProcessors(beanFactory)与registerBeanPostProcessors(beanFactory)中会对已经定义的BeanFactoryPostProcessors实现类和BeanPostProcessors实现类的beandefinition提前实例化


总结:

  • 1、AutotestApplication即启动类时最先被定义的,在实例化时,是除部分processor外最先实例化的,最早执行@postConstruct方法

  • 2、@ComponentScan扫描到的configClass,要比import spring.factories的configclass先放入configurationClass列表,也就是解析的时候要先解析扫描到的configclass,从中解析@Bean方法注册beanDefinition, 静态内部类会被扫描到并注册beanDefinition,但是非静态内部类不会,静态内部类会比外部类更早扫到。

  • 3、@ComponentScan扫描到的configClass,在被扫描到同时会吧整个configclass当成一个bean注册beanDefinition,@import的configclass则不会马上注册beanDefinition,要到将对configClass中的@Bean方法进行注册beanDefinition的时候才先进行configClass的beanDefinition注册。
    (org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass方法中注册beanDefinition)

  • 4、@AutoConfigureBefore和@AutoConfigureAfter只有在使用org.springframework.boot.autoconfigure.AutoConfigurationImportSelector对spring.factories进行加载时,才有对加载出来的configClass进行排序,而ComponentScan扫描出来的并不参与排序,那么configurationClass中,通过spring.factories加载的是有排序过的,但是通过ComponentScan扫描的是没有排序的,但是有时候希望通过@ComponentScan的扫描到的configClass也有排序,那可以通过内部类的配置的方法改变一定的顺序,因为会优先处理内部类,不管静态非静态

  • 5、如果在应用代码中import了某个配置类1,在spring.factories又配置了该配置类1,那么该配置类1是在扫描过程中,在扫描到使用了该import的配置类0,在解析时,首先将配置类1加入到configurationClass列表中,@import再次加载该配置类,就并不会再次加载,只是在importedBy列表中添加第二次import时使用@import的类

  • 6、@import的类又被扫描到的话则扫描的会覆盖@import的,所以通过@import能够被扫描的配置类来打到改变configclass的顺序是无法实现的,由于静态内部类会被扫描到并注册beanDefinition,但是非静态内部类不会,那么可以通过import包含非静态内部类的配置类来达到改变顺序的效果。所有要特别注意引入的jar包的包名会不会跟应用的包名一样,尤其在同一个公司中,包名不能只到机构位置,eg:com.eshin不是一个好的包名,要到com.eshin.autotest,尽可能避免引入的jar包的配置类被扫描到

  • 7、在应用代码中,想要通过spring.factories中的bean作为条件注解判断条件,这时候是会失效的,因为此时,bean的定义中还没有spring.factories中的相关bean的定义,通过spring.factories加载到的configclass,在configurationClass有序集合中是排在比较后面的。此时,若想提前加载spring.factories中某个configclass,可以在应用代码上import某个jar包中需要提前的configClass,但是要小心,提前的这个configClass,是否有依赖jar包中其他的bean的条件注解,或者是否有@AutoConfigureBefore和@AutoConfigureAfter,提前后会失效。


因此:bean定义顺序(即实例化顺序):
1、如果一个configClass被扫描到的话,先定义其被扫描的静态内部类,然后是它本身,接着是它的内部类(由于静态内部类已提前定义,此处可看成仅仅是非静态内部类),然后是import的配置类的定义,然后才是bean方法的定义,类的bean定义,总是先于bean方法的bean定义。
2、如果一个configClass是被import的,那首先是定义它的内部类,不管是静态非静态,然后是内部类的bean方法,最后才是它本身,及其bean方法。但是实例化时,实例化非静态内部类前需要先实例化外部类
3、如果一个configClass又被提前import(使用import的类,比这个configClass先扫描到,eg:hellocontroller比helloconfiguration3先执行),又是扫描的到,那就先定义被扫描到的静态内部类,它本身,然后是非静态内部类,及其bean方法,然后是静态内部类的bean方法,最后才是他本身的bean方法
4、最后实例化就按上述的顺序实例化,但是如果有前面的bean实例化过程中需要autowired后面的额bean,那后面的bean会被提前实例化,如果bean的定义有lazy属性为true,则延后到调用时才实例化

只要理解了configclass递归放入configurationClass列表中的顺序逻辑,就很好理解上面的定义顺序。

验证

根据configurationClass列表和beanDefinitionMap(上面有关于configurationClass列表的图)1-7都是扫描后定义的;8-9能够提前是因为helloController的import,同时是非静态内部类;按configurationClass列表的顺序,15应当在16-17之后的,但是由于在实例化非静态内部类前,需要先实例化外部类,因此15提前。其余的都是按照configurationClass列表中的configClass,定义bean方法的顺序



附:静态内部类和非静态内部类扫描
org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#scanCandidateComponents-->org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#isCandidateComponent(org.springframework.beans.factory.annotation.AnnotatedBeanDefinition)

内部类扫描-非静态.jpg

内部类扫描-静态.jpg

inner2是非静态的,independentInnerClass属性为false,扫描的时候略过,inner20是静态的,independentInnerClass属性true,会扫描到

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349