从源码角度分析Spring 使用@Configuration定义MapperScannerConfigurer引起的同一对象中@Autowired失效的问题 - 草稿

一、问题描述

刚一看标题相信大家都有点懵逼,具体问题是这样的:今天一个群友在群里贴出了一个他遇到的问题,他在使用spring的时候,配置方式是java方式配置,其中的一个配置类大概如下:
配置类

他这个时候发现environment没有被注入进来,然后他把MapperScannerConfigurer的定义去掉后发现,environment可以正常被注入。看到他提出的这个问题后,我写了一个demo测试了一下,果然如他描述,当配置类中定义了MapperScannerConfigurer,这个配置类中Environment是注入不进来的。因为平时喜欢看spring的源码,并且对spring的原理有一定的理解,所以我打算帮他找出导致这个问题的原因。

二、初步分析

为什么加了MapperScannerConfigurer的定义就会出现这种注入不了的问题?MapperScannerConfigurer到底有什么特殊之处,我点开了MapperScannerConfigurer的源码,看了一下:

MapperScannerConfigurer部分源码

这个时候我一眼看到了MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor,XXXPostProcessor是spring生命周期中提供的各类接口,这里我贴出MapperScannerConfigurer的类图,看一下MapperScannerConfigurer的具体继承关系:
MapperScannerConfigurer类图

原来BeanDefinitionRegistryPostProcessor是BeanFactoryPostProcessor的一个子接口,BeanFactoryPostProcessor这个接口的作用是:spring在所有的Bean的定义都被解析完BeanDefinition后,调用所有实现了BeanFactoryPostProcessor接口的类的postProcessBeanFactory方法,来给用户提供最后一个改变BeanDefinition的机会。到这里,我们可以大概有了一个分析的方向。(关于Spring的生命周期,请参考我之前的一篇文章:《BeanPostProcessor和BeanFactoryProcessor浅析》

三、详细分析

我们先从springboot的启动入口开始分析:SpringBoot项目的启动入口为SpringApplication的run方法,我们一步一步进入可以发现最终执行的是SpringApplication的public ConfigurableApplicationContext run(String... args)这个方法。我们把代码拿出来看一下:

run方法

由于本文的重点是排查问题,所以不会详细解释整个启动过程的源码,这里只简单的说一下每一步spring做了什么操作,有兴趣的同学可以自己查阅源码查看细节。

首先,我们可以看到environment初始化和赋值在applicationContext创建的时候就已经完成了,如图中1标记处,environment初始化完成。所以这里可以排除引起标题中说道的问题的一个可能性:MapperScannerConfigurer初始化先于environment。

接着,图中2标记处,创建了applicationContext对象,这里只是创建了一个空的applicationContext对象。

然后,图中3标记处,prepareContext()方法向applicationContext中设置了一些定义性的属性:如设置environment、设置applicationContext的配置定义入口XXApplication(就是我们spring项目中的带main方法的那个入口类)。

最后就是图中标记的第四步,这里是整个springboot项目启动的核心方法。在这一步,spring会加载bean的定义,执行对外开放的生命周期接口。我们进入这个方法,会发现这个方法最后是执行applicationContext的refresh方法。在之前的文章中我们分析过applicationContext的refresh方法。这里,我们以AbstractApplicationContext的refresh方法为例,结合现在的问题再次分析一下这个方法:

refresh方法

图中1处,做刷新applicationContext的准备,这里可以不用特别关心。
图中2处,获取当前applicationContext持有的BeanFactory对象。
图中3处,prepareBeanFactory方法给BeanFactory设置了一些属性,我们也可以忽略。
图中4处,postProcessBeanFactory方法,是一个空方法,提供给子类重写,来作为一个开放点。
图中5处,是本篇文章的关键处,invokeBeanFactoryPostProcessors是干什么的呢?这个方法的作用是执行所有的BeanFactoryPostProcessor接口的postProcessBeanFactory方法。这里有一点我们需要知道,首先不同的Application的子类在图2处的获取BeanFactory的方法处有不同的实现逻辑,有的子类在obtainFreshBeanFactory这个方法中就已经加载了所有的Bean的定义,有的方法没有在此加载Bean的定义,而是将加载Bean的定义放在invokeBeanFactoryPostProcessors中,通过ConfigurationClassPostProcessor这个BeanFactoryPostProcessor的实现类来进行Bean定义的读取。不过不管Bean定义的读取发生在何处,可以确定的是在Bean定义读取完之后会执行所有的BeanFactoryPostProcessor接口提供的生命周期方法。
所以问题就来了,我们把MapperScannerConfigurer定义在我们自己的JAVA配置类中,如果要在图中5处,也就是invokeBeanFactoryPostProcessors方法中被执行,就必须在此之前创建我们的JAVA配置类的实例,也就是我们的栗子中的TestConfig。这个也就是出现我们标题中的问题的关键所在!!!我们要知道这样一个事情:@Autowired注入属性,是通过BeanPostProcessor的一个子类AutowiredAnnotationBeanPostProcessor来实现的,看过之前的《BeanPostProcessor和BeanFactoryProcessor浅析》这篇文章的朋友会知道,BeanPostProcessor这个Spring生命周期中开放的接口是在Bean实例化的时候调用的(postProcessBeforeInitialization方法是在所有的bean的InitializingBean的afterPropertiesSet方法之前执行而postProcessAfterInitialization方法则是在所有的bean的InitializingBean的afterPropertiesSet方法之后执行的),而我们从图中6处可以看到,BeanPostProcessor的注册是发生在BeanFactoryPostProcessor接口被调用之后。而我们的例子中,MapperScannerConfigurer这个类是BeanFactoryPostProcessor的子类,如果想要被创建,并且被应用到图中5处的方法中,就必须先创建TestConfig的实例,而这个时候,BeanPostProcessor接口是没有被注册的,所以这个时候,TestConfig的实例想要通过@Autowired来注入属性对象是不可能的。

四、问题解决

那么问题来了,如果我们在这种情况下想要使用Environment怎么办?这里我先把答案给出来:让配置类实现EnvironmentAware接口,XXXAware也是Spring生命周期中提供的一系列接口,作用是注入一些spring的关键对象,比如说ApplicationContext、Environment等对象。注入的原理也是通过BeanPostProcessor的一个子类ApplicationContextAwareProcessor实现的,在bean被创建的时候如果发现这个bean实现了Aware接口,就调用Aware提供的对应的注入方法,我们可以看一下具体代码:


ApplicationContextAwareProcessor部分代码

细心的朋友也许会问,BeanPostProcessor不是在图中6出注册的吗?应该不会被调用呀。其实是这样的ApplicationContextAwareProcessor作为Spring内部的一个比较重要的类,早在图中3处prepareBeanFactory这个方法中注册了:


prepareBeanFactory方法部分代码

那么问题又来了,这个时候如果我想在例子中的TestConfig配置类中注入其他普通的bean可以吗?答案是不可以!!!我们只能通过实现Aware接口这种手段注入一些Spring给我们提供的特殊对象。或者通过ApplicationContextAware注入applicationContext,从applicationContext中获取需要的bean,但是要注意,尽管applicationContext可以被注入进来,但是在类似MapperScannerConfigurer这种实现了BeanPostProcessor的类的定义方法中,通过applicationContext获取的对象是有问题的,这些对象如果有通过@Autowired注入属性对象,这些属性对象都将是空,甚至直到Spring容器初始化完成之后,这些属性也都是空,所以通过这种方法获取对象,带来的影响太恶劣了,有些得不偿失!
今天的分析就到这里,希望大家能通过本篇,对Spring的生命周期有更深的理解!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容