Spring为什么要用的三级缓存解决循环依赖

一、代码准备

@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

加了三级缓存之后,生命周期如下:

  1. creatingSet

  2. 实例化----->AService不完整对象(new AService())即原始对象—>第三级缓存<‘aSerivce’,lambda(AService原始对象,beanName,Beanefinition)>

  3. 填充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 放入单例池

  4. 填充他属性

  5. 做其他事情

  6. 从二级缓存中取出AService对象

  7. 放入单例池

  8. 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对象而导致失败。

  • 6
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值