面试老大难-SpringBean的依赖注入和循环依赖/三级缓存问题

179 阅读8分钟

大纲

  • Spring Bean的构造推断
  • Spring的依赖注入 @Resource/@Autowired
  • 循环依赖问题和三级缓存
  • 我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

1.Bean的构造推断

Spring的Bean在实例化的时候,创建对象肯定是需要指定构造方法的,要么是无参构造,要么是有参构造,但是如果存在多个构造方法,那么Spring会通过一些策略来选择其中一个构造作为实例化的执行方法。

1.1.没有编写构造方法or显示编写了1个无参构造

这种场景,Spring很好处理,直接通过无参构造实例化得到Bean对象即可。

1.2.只有1个构造且该构造带参

Spring别无选择,只能使用这个构造作为实例化的选择。

     - AnnotationConfigApplicationContext的话,就是用该构造,Spring会根据入参去找Bean,然后传递给构造。
     - ClassPathXmlApplicationContext的话,可以在XML中指定构造方法,也可以配置autowire=constructor让Spring自动寻找Bean作为构造方法的入参。

1.3.有多个构造,且包含1个无参构造

     - 如果用户没有@Autowired指定某个构造,那么Spring使用默认的无参构造。
     - 如果用户指定了某个构造,就使用这个构造。

1.4.有多个构造,不包含无参构造

     - 如果用户@Autowired指定了某个构造,就是用这个构造。
     - 如果用户没有通过注解指定某个构造,则抛异常。
     - 如果多个构造都加了@Autowired,且required都为true,则抛异常。
     - 如果多个构造都加了@Autowired,且只有1required=true,则使用这个构造。
     - 如果多个构造都加了@Autowired,且required都为false,Spring自动选择一个构造。
     - 如果用户都没有通过注解指定,但是在context.getBean传入了构造入参,Spring会根据入参的规则进行匹配,优先选择匹配度最高的构造,有相同参数情况下优先参数最多的那个。

依赖注入

2.手动依赖注入

在XML配置的形式下,在标签中可以手动注入属性的ref引用关系或者指定构造的注入,称之为手动注入的形式(set注入、构造注入)。

3.XML形式的自动注入

在标签中可以配置autowire的自动注入方式是byType还是byName还是构造等,在创建Bean的过程中Spring将这个类的所有方法解析出来通过一定的规则进行筛选(例如set方法)然后进行属性注入。

4.@Autowired自动注入原理

4.1 @Autowired特点

它是byType和byName的结合,注解可以使用在:

        - 属性:优先byType寻找,如果存在多个则再根据名字查找。
        - 构造:优先根据方法参数类型byType寻找,如果存在多个则再根据名字查找。
        - set方法:优先根据方法参数类型byType寻找,如果存在多个则再根据名字查找。

4.2 寻找注入点(postProcessor机制实现)

在Bean的实例化阶段时,Spring利用AutowiredAnnotationBeanPostProcessor拓展机制调用#postProcessMergedBeanDefinition()方法来对Autowired注解的注入点寻找。寻找的流程大致为:

     1. 遍历当前Bean类的所有字段属性。
     1. 检查这个属性上是否加了`@Autowired、@Value、@Inject`的其中任意一个注解,如果有,则认为这是一个注入点。
     1. 判断字段是否是static静态修饰的,如果是,则跳过,不进行注入。
     1. 获取Autowired注解中的required属性值(这个值代表注入是否可以非null,默认是false)。
     1. 将这个属性字段封装成一个`AutowiredFiledElement`对象,放入Elements集合中。
     1. 开始这个Bean类的所有方法遍历。
     1. 检查这个方法上是否加了`@Autowired、@Value、@Inject`的其中任意一个注解,如果有,则认为这是一个注入点。
     1. 如果方法时静态方法,则跳过,不视为注入点。
     1. 获取Autowired注解中的required属性值(这个值代表注入是否可以非null,默认是false)。
     1. 将这个方法封装成一个`AutowiredFiledElement`对象,放入Elements集合中。
     1. 返回这个注入点集合。

4.3 注入

Spring基于AutowiredAnnotationBeanPostProcessor#postProcessProperties()方法中,遍历所有的注入点,然后依次注入调用。

4.3.1 属性注入

从BeanFactory容器中调用resolveDependency方法,进行Bean对象的查找,通过反射进行值的注入。

4.3.2 set注入

和属性注入一样,从BeanFactory中寻找并通过反射invoke,让对象调用方法执行注入。

5.@Autowired流程

image.png

6.@Resource流程

image.png

循环依赖问题

7.循环依赖的产生

public class A {
    @Autowired
    private B b;
}

public class B {
    @Autowired
    private A A;
}

Spring默认是支持循环依赖的,不过也可以通过容器设置关闭。Spring的循环依赖是基于三级缓存的三个Map来完成的,三个Map各自有各自的作用。

8.Bean的生命周期总览

  1. 包扫描、BeanDefinition的生成。
  2. 类加载、BeanDefinition准备生成对应的Bean。
  3. 通过构造推断,选定构造方法,实例化出一个Bean。
  4. 填充属性、依赖注入。
  5. 初始化
  6. 初始化后,判断该Bean是否需要AOP,如果需要则AOP。
  7. 将最终生成的Bean放入单例池singletonObjects这个Map中。

9.三级缓存

image.png 传统的循环依赖,不依靠外力干扰肯定是不能实现的,需要一些Map进行实例化过程中的外力干扰。Spring提出了三级缓存的概念,其实就是3个Map:

  - `singletonObjects`:单例池,保存着完整实例化后的Bean,可以直接通过getBean( )获取。
  - `earlySingletonObjects`:残缺单例池,保存着还没实例化完成的Bean(没有注入完成的当前								Bean或者是提前AOP的Bean)。
  - `singletonFactories`:单例工厂,保存的是一个Lamda表达式,表达式的程序执行结果就是要							放入二级缓存的对象。

9.1 循环依赖的解决流程

A在实例化过程中,会提前将创建Bean的一段程序代码:Lamda表达式暴露出来,这个Lamda表达式有可能用不上,也有可能用得上。将这个Lamda表达式缓存在三级缓存singletonFactories这个Map中。 如果发生了循环依赖,B在获取A的时候,先去一级缓存拿,拿不到就去二级缓存拿,如果还是拿不到,则去三级缓存拿(三级缓存一定能拿到),三级缓存拿到的是一个Lamda表达式程序,然后调用这个程序(如果A需要AOP,则生成代理,否则返回A原始对象),拿到以后放入二级缓存且删掉三级缓存,注入给B的A属性。 实例化完成后将完整的Bean,放入单例池。

9.2 singletonFactories存在的必要?

因为SpringBean的声明周期中存在AOP阶段,而AOP产生的对象和原始对象是不同的对象。如果循环依赖没有第三级的缓存,那么在二级缓存里面就是原始对象,而不是最终的AOP代理对象。 当然Spring可以在实例化后,注入之前提前进行AOP的操作,但是这个流程不符合Spring的设计。

9.3 原型Bean的循环依赖

Spring对原型Bean不支持循环依赖,如果发生了会启动报错。因为原型Bean每次都会创建新的对象,包括对象里的属性都是新创建。如果发生了循环依赖,则陷入死循环无法处理而报错。

9.4 构造注入的循环依赖问题

构造注入仍然不支持循环依赖,会报错。和原型Bean的创建类似,通过构造创建A对象,那么B对象必须作为入参,如果B对象都找不到那么构造都无法完成,无法进行下去。

10.@Async遭遇循环依赖的启动报错问题

场景:A依赖B,B依赖A,A中方法加有@Async注解 如果@Async注解+循环依赖会造成启动报错,底层根本原因是给B的A属性注入之后,会检查这个A属性和singltonObjects单例池里A对象是否是同一个,如果不是就会抛这个异常。而@Async注解为什么会改变这个对象,是因为@Async注解是基于beanPostProcessor的,A初始化完成后会基于AOP产生新的对象。而@Transactional不会造成这个问题,是因为事务注解不是基于beanPostProcessor而是基于advisor。 解决这个问题可以使用@Lazy来解决、或者让Spring对B先初始化然后初始化A,就不会有这个问题。