@Bean放入其引用Bean中初始化失败分析

本文探讨了SpringBoot框架下Bean依赖注入的顺序问题,特别是当内层Bean依赖于外层Bean的属性时,由于@Resource和@Value注解的注入逻辑不同,可能导致属性注入的顺序出乎意料。作者通过调用栈追踪分析了问题出现的原因,并建议将@Bean配置的Bean放在@Configuration类中以避免此类问题。
摘要由CSDN通过智能技术生成

以下讨论的问题及术语均在SpringBoot框架下,问题十分小众,仅做整理记录。


1. 先说重点

  1. Bean依赖属性的注入顺序,与代码定义顺序无关;
  2. 最好是将@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依赖initValueneeInitBean两个属性,读配置拿到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

    resource

  6. 继续跟着调用栈往下走,到AbstractAutowireCaptableBeanFactory中,发现有一个循环去遍历BeanPostProceccer, 并过滤出InstantiationAwareBeanPostProcessor,对创建中的Bean进行处理,展开BeanPostProceccer的列表,会发现我们上边看到的CommonAnnotationBeanPostProcessor后边还有个AutowiredAnnotationBeanPostProcessor,此类也继承自InstantiationAwareBeanPostProcessor, 所以也会遍历到,然后我们就会发现他与5中描述的逻辑类似,也是先找到需要注入的属性,然后执行注入。不同的是它解析@Value@Autowire注解的属性为需要注入的属性;
    在这里插入图片描述

  7. 6中提到的遍历逻辑,是在对外层Bean进行依赖注入,即外层Bean的初始化过程,因为外层Bean是@Service注解修饰的,所以会在SpringBoot启动时扫描到进行初始化,所以我们再往下走没几步就到了SpringApplication.run


4. 问题出现逻辑梳理

  1. 应用启动,扫描@Service注解修饰的外层Bean,对其进行初始化;

  2. Bean的初始化由若干实现InstantiationAwareBeanPostProcessor接口的类在一个循环中依次对Bean进行处理;

  3. 循环中负责依赖注入的类CommonAnnotationBeanPostProcessor发现属性needInitBean@Resource修饰,需要进行注入,此时BeanFactory中没有needInitBean这个Bean,故对其进行初始化,此时外层Bean的initValue还没有注入进来,所以内层Bean初始化needInitValuenull

  4. 循环中负责依赖注入的类AutowiredAnnotationBeanPostProcessor发现属性initValue@Value修饰,需要进行注入,执行注入;

  5. 完成外层Bean的创建;


5. 结论

通过上述追踪,我们可以得出出现我们最初问题的原因:由于@Value@Resource在注入时并非用一个类进行注入,存在先后关系,故虽然外层Bean已经初始化一半去初始化内层Bean,initValue仍然没有值。

另外退一步说,如果我们使用的是@Autowire,而不是@Resource@Autowire@Value是由同一个BeanPostProceccer进行注入的,是不是@Value写在前面,本程序就能通呢?运行了一下是可以的,然而这并不严谨,因为就算是同一个BeanPostProceccer进行注入, 其属性的注入顺序是依赖反射包下的Class.getDeclaredFields方法获得的,而此方法注释明确写道,返回的数组是无序的

所以我们尽量还是避免这种写法,将@Bean注解配置的Bean放在@Configuration注解修饰的专门用于配置的类中较为稳妥。

ps: 如果我们将initValue使用属性注入,而needInitBean使用@Autowire修饰setter注入,可以保证严谨,因为目前的实现都是先进行属性注入在进行方法注入,不提倡。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在 Spring ,我们可以通过定义 Bean 来管理我们的 Java 对象。Bean 是由 Spring 容器来创建、组装和管理的。 在容器启动时,Spring 会读取配置文件(如 XML、JavaConfig 等)Bean 定义,并通过反射机制创建 Bean 对象,然后将这些 Bean 对象放入容器进行管理。 在创建 Bean 对象时,Spring 提供了多种初始化方式: 1. 实现 InitializingBean 接口,重写 afterPropertiesSet() 方法,该方法会在 Bean 实例化后、属性注入完成后被调用。 2. 在配置文件使用 init-method 属性,该属性指定一个自定义的初始化方法,Spring 容器会在 Bean 实例化后、属性注入完成后调用该方法。 3. 使用 @PostConstruct 注解标注一个自定义的初始化方法,该方法会在 Bean 实例化后、属性注入完成后调用。 4. 在配置文件使用工厂方法创建 Bean,该方法返回的对象就是要创建的 Bean,同时可以指定一个自定义的初始化方法,Spring 容器会在 Bean 实例化后、属性注入完成后调用该方法。 以上方法都可以用来实现 Bean 的初始化操作。需要注意的是,在使用以上方法时,Bean 的属性已经被注入,所以可以在初始化方法使用 Bean 的属性。 同时需要注意的是,如果 Bean 实现了 DisposableBean 接口或者配置了 destroy-method 属性或者使用 @PreDestroy 注解标注的销毁方法,Spring 容器会在容器关闭时调用销毁方法,进行资源的释放等操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孔天逸

没有钱用,只能写写博客这样子~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值