大纲
- 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,且只有1个required=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流程
6.@Resource流程
循环依赖问题
7.循环依赖的产生
public class A {
@Autowired
private B b;
}
public class B {
@Autowired
private A A;
}
Spring默认是支持循环依赖
的,不过也可以通过容器设置关闭。Spring的循环依赖是基于三级缓存的三个Map来完成的,三个Map各自有各自的作用。
8.Bean的生命周期总览
- 包扫描、BeanDefinition的生成。
- 类加载、BeanDefinition准备生成对应的Bean。
- 通过构造推断,选定构造方法,实例化出一个Bean。
- 填充属性、依赖注入。
- 初始化
- 初始化后,判断该Bean是否需要AOP,如果需要则AOP。
- 将最终生成的Bean放入单例池
singletonObjects
这个Map中。
9.三级缓存
传统的循环依赖,不依靠外力干扰肯定是不能实现的,需要一些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,就不会有这个问题。
- 我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。