spring-2-依赖注入、循环依赖、三级缓存
参考:
Spring注解@Resource和@Autowired区别
第二次讲Spring循环依赖,时长16分钟,我保证每一秒都是精华
1.依赖注入
1.1 依赖注入的方式
方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
属性注入 | 简洁,减少样板代码,直接在字段上注入依赖。 | 不利于测试,无法使用final 字段,可能导致类的职责不清晰。 |
适用于简单的依赖注入,测试和重构要求不高的场景。 |
Setter 注入 | 灵活,易于理解和维护,支持可选依赖注入,Setter方法可以包含验证逻辑。 | 需要额外的Setter方法,可能导致代码冗长,依赖不能为final 字段。 |
适用于依赖可选且可能变化的场景,需要在注入前进行额外处理。 |
构造方法注入 | 强制依赖注入,保证依赖完整性,支持final 字段,有助于单元测试。 |
需要额外的构造函数,可能在有多种依赖时显得冗长。 | 适用于依赖项多且不可或缺的场景,需要进行严格依赖管理和单元测试。 |
上面3个呢,就是分别把咱们的@Autowired
注解用在属性上、set方法上、构造方法上。
方便演示,修改下之前的工程,添加一个实体类User,在MyConfig中注册为Bean。
然后我们用一个UserService测试下。
1.1.1 属性注入
属性注入很简单,就是我们最常用的那样,直接在属性上@Autowired
。
@Slf4j
@Service
public class UserService {
@Autowired
private User user;
void showInfo() {
log.info("user: {}", JSON.toJSONString(user));
}
}
1.1.2 Setter方法注入
setter注入,就是把注解用在set方法上。
@Slf4j
@Service
public class UserService {
private User user;
@Autowired
public void setUser(User user) {
this.user = user;
}
void showInfo() {
log.info("user: {}", JSON.toJSONString(user));
}
}
1.1.3 构造方法注入
@Slf4j
@Service
public class UserService {
private User user;
@Autowired
public UserService(User user) {
this.user = user;
}
void showInfo() {
log.info("user: {}", JSON.toJSONString(user));
}
}
1.2 依赖注入原理
现在,我们来研究下UserService中的user对象怎么来的。
还记得前面嘛,我们反射创建Bean后,会根据Bean中的属性来进行注入。
直接debug到之前提到的。
// 实例化所有剩余的(非lazy-init)单例。
finishBeanFactoryInitialization(beanFactory);
位于org/springframework/context/support/AbstractApplicationContext.java
F7直接步入进去,看咋创建。
嗯,上一步的入参是beanFactory,这里直接调用beanFactory的方法。
继续进去,来到了preInstantiateSingletons的方法内部,哎?this是谁,this不就是咱们beanFactory自己吗。
右键加上条件断点,注意啊,我是加在for里面的那行,条件断点不就是让代码执行到满足条件的情况嘛。
然后下面的if又判断是不是FactoryBean,我们不是,所以,咱直接关掉这个if,不就到了创建了嘛。
之后的过程就比较复杂了,会走到org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java
的populateBean方法中。
把条件断点打成
bp.getClass().getName().contains("AutowiredAnnotationBeanPostProcessor")
这里,就是根据不同的bp(BeanPostProcessor)来执行对应的postProcessProperties方法。
比如,我们此处条件断点到了。
而我们的org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java
就是在注入Autowired的属性。
经过这个方法后,其中的user对象已经能够被注入了。
那么这个AutowiredAnnotationBeanPostProcessor.java中具体的注入逻辑是啥呢。
没事,我们直接把干扰的代码去掉。
@Override
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
Field field = (Field) this.member;
// 查找值
Object value = this.cached ? resolvedCachedArgument(beanName, this.cachedFieldValue) : resolveFieldValue(field, bean, beanName);
// 注入值
ReflectionUtils.makeAccessible(field);
field.set(bean, value);
}
好,那注入的逻辑就是BeanPostProcessor的不同,比如我们换成通用注解@Resource,那这里的条件断点就修改成这样。
1.3 @Autowired和@Resource
项目 | @Resource | @Autowired |
---|---|---|
出处 | Java注解 (javax.annotation.Resource) | Spring注解 (org.springframework.beans.factory.annotation.Autowired) |
默认自动装配策略 | 名称优先 | 类型优先 |
名称自动装配 | 支持 | 通过@Qualifier支持 |
类型自动装配 | 名称未找到时按类型 | 默认 |
可空性 | 不适用 | 可以为null(required=false) |
类级别注解 | 支持 | 不适用 |
2.循环依赖
2.1 是什么
循环依赖问题,讲的就是两个Bean对象互相依赖,你中有我,我中有你。
例如下方的UserService其中需要注入我们的PasswordService,而PasswordService呢又需要一个UserService。
- 要创建UserService,你得把PasswordService先给我呀。
- 要创建PasswordService,你得把UserService先给我呀。
???
@Slf4j
@Service
public class UserService {
@Resource
private PasswordService passwordService;
}
@Service
public class PasswordService {
@Resource
private UserService userService;
}
碰到这种场景,启动的时候会给你明显的报错信息,告诉你有循环依赖的问题。
当然咯,以下这些场景都是类似的。
2.1.1 自己依赖自己
2.1.2 双方互相依赖
2.1.3 多方互相依赖
2.2 解决方案
2.2.1 优化代码
好,那么什么场景下可能导致这个问题,以上方的UserService和PasswordService为例。
那他们既然在内部注入了,肯定就是:
- UserService中想用PasswordService
- PasswordService中想用UserService
最简单的方式,就是抽离出第三方类,聚合这两个玩意,让它们不要在内部互相依赖。
@Service
public class CommonService {
@Resource
private UserService userService;
@Resource
private PasswordService passwordService;
}
然后将我们涉及到依赖的逻辑抽离到这个CommonService中,用CommonService来完成操作。
2.2.2 三级缓存
2.2.2.1 回顾Bean的创建
1.普通的Bean
首先呢,我们看下Bean是怎么获取的,从我们的BeanFactory的单例池中来拿到。
单例池中没有,我们就会尝试创建这个Bean。
下方演示一个简单的bean对象创建,其中包含一个属性id。
-
获取Bean:getBean(a) 获取这个bean(发现没有)
-
实例化:没有,则反射创建这个Bean实例。
-
填充属性:得到Bean实例后,通过populateBean()来填充其中的属性id。
-
初始化:最后执行下我们的初始化方法,例如之前提到的postProcessAfterInitialization(在bean初始化之前执行)的一些初始化方法。
-
放到单例池中去
2.属性中包含别的对象的bean
那么假设对象A中还有个对象B呢?无非就是填充属性的时候,把这个B创建出来。
3.属性产生循环依赖的bean
当B对象中需要A对象时,创建则会产生死循环。
- a对象中需要依赖b对象,则开始b对象的创建。
- b对象中执行属性填充的时候,尝试获取a对象。
- 由于此时a对象也还没创建出来(单例池中没有a),那么又会触发a对象的创建。
- 死循环
2.2.2.2 三级缓存的引入
回顾上方的内容,我们发现问题在哪。
问题在于,填充属性的时候,getBean在单例池中没有对象可以拿,没法填充了。
所以,基本的思路就是,反射实例化后,我们先把这些对象放到半成品的池子里(没有填充对象,半成品)。
在单例池中拿不到时,尝试从半成品池子中拿。
先看b填充a属性的那里,虽然单例池拿不到a,但是我们可以从半成品池子中拿到a,进而走完Bean对象b的创建。
拿到半成品的a后,完成b的创建,最后b被放到了单例池中去。
最后,对象a在填充属性b的时候,单例池中已经有b能填充了,a对象也能完成创建。
创建完成a后,检查下把无意义的半成品池中的a删除掉即可。
至此,a和b都拿到了实际的对象(非半成品对象),创建完成。
哎,看起来2级缓存已经够用了,为啥spring最后搞的是三级缓存?
因为咱们Spring中创建出来的不是普通对象,最后Spring要交付的是经过AOP代理的对象。
即,当对象a中注入属性b的时候,注入的可不能是仅仅简单实例化的半成品b,而是要注入代理对象proxy$b
。
半成品池子中,存放的是未属性填充的半成品对象,不是AOP对象。
如果是aop代理,经过填充后,b里面的a是不对的。
那么这个代理对象是在啥时候创建的?在初始化阶段通过我们的postProcessAfterInitialization来创建的。
这个PostProcessor就是AspectJAwareAdvisorAutoProxyCreator
。
AspectJAwareAdvisorAutoProxyCreator
中有两个重要的方法。
-
postProcessBeforeInstantiation
决定是否要为当前 Bean 创建一个代理对象。如果需要代理,就返回代理对象;否则返回
null
,继续正常的 Bean 实例化过程。 -
getEarlyBeanReference
返回一个可能的代理对象,确保在循环依赖情况下,其他 Bean 获取到的是这个 Bean 的代理对象而不是原始对象。
所以,现在加上AOP后,实际上应该是这个样子。
以对象a为例
- 在初始化时,执行a的后置处理器创建代理对象proxy$A。
- 将proxy$A放回单例池
回到循环依赖的问题,以b填充a属性为例,关键点是我们属性填充的时候,填不出来一个动态代理的proxy$A啊。
要是可以把初始化阶段的后置处理提前就好了
实际上就是这样做的,我们把创建代理动态代理的步骤提前,打包成一个工厂方法,故而产生了我们的第3个池子,工厂池。
我们调用这个工厂池中跟这个对象绑定的工厂方法,从而提前的来创建出这个动态代理。
所以,我们就能把半成品池子中的对象替换成最后需要的代理对象。
经过上面的操作后,我们单例池中就能存放到实际的代理对象,解决掉我们放不进去代理对象的问题。
最后回顾下整个过程中的三个缓存池。
- singletonObjects: 单例bean缓存(完整的Bean对象)
- earlySingletonObjects: 早期单例bean缓存(半成品对象)
- singletonFactories: 单例bean工厂缓存(用于创建代理对象的工厂对象)