关于spring的@Bean注解放入引用Bean中初始化失败分析
作者:孔天逸
以下讨论的问题及术语均在SpringBoot框架下,问题十分小众,仅做整理记录。
1. Bean依赖属性
Bean依赖属性的注入顺序,与代码定义顺序无关;
最好是将@Bean
注解配置的Bean放在@Configuration
注解修饰的专门用于配置的类中;
2. 问题背景
为了方便,将使用注解(@Bean
)方法生成的Bean的方法体定义在了使用此Bean的类中
代码结构如下(为了描述方便,后文我们姑且将initBeanTestService
叫做外层Bean,needInitBean
叫做内层Bean):
编写单元测试
运行printInitBeanValue
方法,并在方法体内打断点便于观察属性值,
单元测试:
运行单元测试会发现,通过内层Bean的属性值needInitValue
的值为null
,而外层Bean的属性值needInitValue
有值
说明在初始化needInitBean
时,外层Bean的属性值initValue
并未注入成功,
运行结果:
简单理下思路,因为外层Bean的类通过@Service
注解进行修饰,所以SpringBoot在启动时会扫描到此注解进行Bean的初始化
初始化时会发现此Bean依赖initValue
和neeInitBean
两个属性,读配置拿到initValue
的值
然后去容器中查找是否有needInitBean
存在,显然并不存在,于是要先初始化needInitBean
,即内层Bean;
内层bean的初始化,依赖于外层bean的initValue
属性值
从现象来看,此时initValue
无值,我们有以下疑问:
此initValue为什么没有值?外层Bean按理说应该已经初始化一半了。
3. 调用栈追踪
为了解释上述问题1,我们在@Bean
注解修饰的方法体内打断点,从内层Bean的初始化开始,沿着断点处的调用栈倒着追踪,
1.首先是一些反射包下的方法;
2.一些BeanFactory初始化bean的方法;
3.找到AbstractBeanFactory
中,发现此处开始创建needInitBean
,那么上边的调用方就是初始化此Bean的触发点;
4.找到CommonAnnotationBeanPostProcessor
,发现是此处为触发点;
5.在CommonAnnotationBeanPostProcessor
一番游历,发现此处的逻辑是向外层Bean中注入依赖,找到319行,findResourceMetadata
,此方法为找到需要注入的属性或方法的元数据,紧接着321行,为依赖注入逻辑(当然,若依赖是Bean,则去BeanFactory请求,找不到则进行初始化);
点进去findResourceMetadata
方法看看他是咋找要注入的属性的,包了一层缓存,主要逻辑在buildResourceMetadata
方法,这里我们会发现,他遍历了各个属性和方法,找到有特定注解的属性和方法,放到了待注入的列表。其中注解就包括了我们熟悉的,也是外层bean中needInitBean
头上的@Resource
。但是并没有发现我们同样熟悉的@Value
和@Autowire
;
6.继续跟着调用栈往下走,到AbstractAutowireCaptableBeanFactory
中,发现有一个循环去遍历BeanPostProceccer
, 并过滤出InstantiationAwareBeanPostProcessor
,对创建中的Bean进行处理,展开BeanPostProceccer
的列表,会发现我们上边看到的CommonAnnotationBeanPostProcessor
后边还有个AutowiredAnnotationBeanPostProcessor
,此类也继承自InstantiationAwareBeanPostProcessor
, 所以也会遍历到,然后我们就会发现他与5中描述的逻辑类似,也是先找到需要注入的属性,然后执行注入。不同的是它解析@Value
和@Autowire
注解的属性为需要注入的属性;
7.上面提到的遍历逻辑,是在对外层Bean进行依赖注入,即外层Bean的初始化过程,因为外层Bean是@Service
注解修饰的,所以会在SpringBoot启动时扫描到进行初始化
所以我们再往下走没几步就到了SpringApplication.run
4. 问题出现逻辑梳理
- 应用启动,扫描
@Service
注解修饰的外层Bean,对其进行初始化; - Bean的初始化由若干实现
InstantiationAwareBeanPostProcessor
接口的类在一个循环中依次对Bean进行处理; - 循环中负责依赖注入的类
CommonAnnotationBeanPostProcesso
r发现属性needInitBean
有@Resource
修饰,需要进行注入,此时BeanFactory中没有needInitBean
这个Bean,故对其进行初始化,此时外层Bean的initValue
还没有注入进来,所以内层Bean初始化needInitValue
为null
; - 循环中负责依赖注入的类
AutowiredAnnotationBeanPostProcessor
发现属性initValue
有@Value
修饰,需要进行注入,执行注入; - 完成外层Bean的创建;
5. 结论
通过上述追踪,我们可以得出出现我们最初问题的原因:由于@Value
和@Resource
在注入时并非用一个类进行注入,存在先后关系
故虽然外层Bean已经初始化一半去初始化内层Bean,initValue
仍然没有值。
另外退一步说,如果我们使用的是@Autowire
,而不是@Resource
,@Autowire
和@Value
是由同一个BeanPostProceccer
进行注入的
是不是@Value写在前面,本程序就能通呢?
运行了一下是可以的,然而这并不严谨,因为就算是同一个BeanPostProceccer
进行注入, 其属性的注入顺序是依赖反射包下的Class.getDeclaredFields
方法获得的,而此方法注释明确写道,返回的数组是无序的。
所以我们尽量还是避免这种写法,将@Bean
注解配置的Bean放在@Configuration
注解修饰的专门用于配置的类中较为稳妥。
ps: 如果我们将initValue
使用属性注入,而needInitBean
使用@Autowire
修饰setter
注入,可以保证严谨,因为目前的实现都是先进行属性注入在进行方法注入,不提倡。
到此这篇关于关于spring的@Bean注解放入引用Bean中初始化失败分析的文章就介绍到这了,更多相关spring@Bean注解引用Bean内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!