一、代码准备
@Component("aService")
public class AService(){
@Autowired
private BService bService;
public void test(){
System.out.println(bService);
}
}
@Component("bService")
public class BService(){
@Autowired
private AService aService;
}
@ComponentScan("com.test")
public class AppConfig {
}
public class Test {
public static void main(String[] args){
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
AService aService = (AService)applicationContext.getBean("AService");
aService.test();
}
}
在AService中依赖了BService,Bservice中依赖了AService。这种是通常认为会出现循环依赖的一种情况。
可以先执行下代码看下结果
正常打印出了BService
二、到底什么是循环依赖?
对于AService和BService来说,都是bean对象。
而要创建bean对象是有一定步骤的。
假设先创建AService,以下为创建bean对象的一些基本步骤
1.创建一个AService普通对象(new AService)
2.填充BService属性----》去单例池中获取BService—》找不到,回去创建一个BService的Bean对象—》执行BService的生命周期----》(和A相同),在填充AService属性时—》去单例池中获取AService—》找不到,回去创建一个AService的Bean对象—》执行AService的生命周期…
3.填充其他属性
4.其他操作
5.初始化后
6.放入单例池
进入了循环依赖
更详细的描述,可前往Spring循环依赖过程解析查看
三、三级缓存
第一级缓存:单例池
singletonObjects ConcurrentHasnMsp<beanName,bean对象>
作用:保证一个beanName对应唯一的Bean完整对象
第二级缓存
earlySingletonObjects HashMap<beanName,bean对象>
作用:保证一个beanName对应唯一的Bean不完整对象
属性暂时没有值的对象称之为不完整的Bean对象(还没有走完生命周期)
比如A、B、C三个类,B、C中依赖 A, A中依赖B、C
在创建A时,保证注入B C,创建时拿到的A的不完整对象是同一个。
第三级缓存
singletonFactory HashMap<beanName,ObjectFactory(lambda表达式)>
作用:做一些预备工作。创建bean的时候(第一步实例化产生的对象)先存到三级缓存,并不知道后面逻辑会不会用,会不会出现循环依赖等,只是防止出现循环依赖且AOP等场景。
三级缓存是真正打破循环的map
加了三级缓存之后,生命周期如下:
-
creatingSet
-
实例化----->AService不完整对象(new AService())即原始对象—>第三级缓存<‘aSerivce’,lambda(AService原始对象,beanName,Beanefinition)>
-
填充bService属性—>从单例池中找bService—>创建bService
bService的生命周期
3.1 实例化…BService对象(new BService())
3.2 填充aService属性—>从单例池中找aService—>找不到—>在cretaingSet中----->aService正在创建中—>aService出现了循环依赖—>从二级缓存中查找
二级缓存找到对应的bean对象,直接拿来使用,继续向下执行,进行3.3
二级缓存没有找到对应的bean对象----->—>提前AOP----->第三级缓存—>执行lambda—>得到代理对象—>放入第二级缓存<‘aSerivce’,AService代理对象>
3.3 填充他属性
3.4 做其他事情
3.5 放入单例池
-
填充他属性
-
做其他事情
-
从二级缓存中取出AService对象
-
放入单例池
-
creatingSet.remove(“aService”)
简单说:
先从单例池中查找,找到直接使用。
没有找到,从二级缓存中找,找到直接使用
没有找到,从三级缓存中找,执行lambda,得到对象放入二级缓存。
部分源码解释
从上述定义,可看出:一级缓存(单例池)使用的是ConcurrentHashMap,二级缓存和三级缓存都是用的HashMap
为什么二级缓存和三级缓存用HashMap而不是使用ConcurrentHashMap呢?难道就不考虑线程安全的问题吗?
由于三级缓存中存的是lambda表达式且是一次性的,只要执行过一次就会被移除。二级缓存中存的是三级缓存lambda执行的结果。
也就是说同一个bean的名字,在三级缓存中如果存在一个lambda表达式,那么就表示在二级缓存中beanName对应的就没有值。同样的,反过来,在二级缓存里面beanName有值,那么在三级缓存中就没有对应的表达式。相当于是原子性的。
从第二段代码中可以看出,在往二级缓存push的时候,会把三级缓存的数据清除掉,那么就必须保证操作的原子性。很显然,二级缓存和三级缓存定义为ConcurrentHashMap并不能保证操作的原子性。只能添加synchronize加锁控制。
结合第二三段代码,可以看到,两个map的操作总是在一起的,添加到一个里面就从另一个中移除,同时加锁控制,已经保证了并发的操作安全,所以就没有必要设置为ConcurrentHashMap,在这种前提下,考虑性能,选择了HashMap。
在比较新版本代码中,二级缓存换成了ConcurrentHashMap。因为在锁外面会用到二级缓存。原来的旧代码就还是HashMap
四、@Async为什么会导致循环依赖
加上异步注解,执行程序看下效果
@ComponentScan("com.test")
@EnableAspectJAutoProxy
@EnableAsync
public class AppConfig {
}
@Component
public class AService {
@Autowired
private BService bService;
@Async
public void test(){
System.out.println(bService);
}
}
运行之后发现报错:
Error creating bean with name ‘AService’: Bean with name ‘AService’ has been injected into other beans [BService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using ‘getBeanNamesOfType’ with the ‘allowEagerInit’ flag turned off, for example.
看报错是:循环依赖的问题。
注释掉异步注解,就可以正常执行了,这是为什么呢?
在Spring中@Async注解也有自己对应的切面逻辑
点击报错信息,可以看到是在这里抛出的异常
判断到AService循环依赖了,会在这里执行lambda表达式,这里执行的切面逻辑是自己自定义的逻辑
然后继续往下走:
假设先执行的自定义切面的beanPostProcessor
然后再执行的@Aysnc的beanPostProcessor。并没有任何判断,直接设置处理了。
因为出现了循环依赖,AService在自定义切面逻辑里面已经生成了代理对象,进入下一次循环时,Async切面又会生成一个代理对象,而这两个代理对象肯定是不一样的。所以在后面抛出了异常
如果逻辑必须要这么处理,怎么来解决循环依赖呢?
可以使用@Lazy注解解决
@Component
public class AService {
@Autowired
private BService bService;
@Async
@Lazy
public void test(){
System.out.println(bService);
}
}
加了@Lazy注解后;
在创建AService的过程中,需要给属性BService赋值,直接赋值BService的代理对象,不管BService当前是什么情况,并不会从sprig容器中去找BService的bean对象而是直接生成BService的代理对象。
在真正使用BService对象的时候才会去Spring容器中去找对应的bean对象。
调用方法的时候,说明AService的生命周期已经执行完成,再创建BService的时候就不会出现循环依赖了。
五、构造方法和多例导致的循环依赖
构造方法
上面的代码,使用的都是默认的构造方法来生成的对象,如果是指定特定构造方法,会有什么问题呢?
比如没有用属性注入而是使用构造方法注入,以下代码:
@Component
public class AService {
private BService bService;
public AService(BService bService){
this.bService = bService;
}
public void test(){
System.out.println(this.bService);
}
}
@Component
public class BService {
private AService aService;
public BService(AService aService){
this.aService = aService;
}
}
执行报错:
创建AService的bean对象,只能使用给出的构造方法,但是构造方法里面需要一个BService,发现找不到。
找不到就会去创建BService对象,也是只能使用给出的构造方法,需要一个AService的bean,不能再去创建AService了,但是又没有办法得到一个AService对象。
也就是说,在创建普通对象的时候就失败了,所有没有办法生成lambda表达式,更没有办法做其他事。
对应的解决办法也有,加上@Lazy注解即可。Spring会传入一个@Lazy对应的代理对象参数进入
多例
不论是AService、BService、AService+BService是原生bean,在创建使用指定构造方法的时候(使用上述的构造方法)
都会由于需要另一个bean对象而导致失败。