详解Spring的循环依赖

本篇博客为学习哔哩哔哩中的黑马程序员的spring复习视频的学习笔记,仅供参考。

目录

代理的创建时机

aspectj与advisor的关系

自动后置代理处理器

循环依赖

set注入导致的循环依赖以及spring中的三级缓存

构造注入的循环依赖以及解决方案

小结

代理的创建时机

要想完全的理解循环依赖,需要理解代理的创建时机,需要知道ProxyFactory 创建代理的过程,需要理解advisor,advice,pointcut与aspecj的关系,还需要知道后置代理处理器的一些功能,后面都会讲解。

 执行结果:

注意:使用jdk提供的proxy代理实际上是对代理工厂proxyFactory实现的接口进行代理增强,使用接口进行代理的话,目标对象与代理对象是平级的关系。 如果没有实现接口,使用cjlib代理的话,生成的代理对象(子)与目标对象(父)是父子关系,目标对象是父类,生成的代理对象是子类。

 

其实上面的通知就已经可以完成对目标对象进行增强,但是只是单纯的使用通知来进行增强那么它会为目标对象的所有方法都进行增强从而导致匹配不够精确,所以这里就可以引入切点了。当切点也有了,通知也有了,那么它们俩是怎么结合起来的?通过advisor把切点和通知结合起来。

 

aspectj与advisor的关系

aspect = 通知(advice)+ 切点(pointcut),【一个切面类中可能有一个到多个通知方法】。

advisor = 更细粒度的切面,包含一个通知和对应的切点。

代理对象调用方法的过程:代理对象的内部先找到切点,然后通过切面去检查切点是不是跟方法先匹配,如果方法与切点先匹配了那么才会去调这个通知,这里就抛出一个问题:这个切面的信息都被保存在哪里去了?不管是基于jdk的proxy还是基于cjlib的类代理,实际上这些切面信息都是被保存在代理对象的advisors成员变量中

aop编程三要素:切面(advisor),切点(pointcut),通知(advice)

我们在实际进行aop编程的时候使用的都是注解形式的开发,@aspecj表面一个类是切面类,切面类中标注@around,@before,@after等注解的方法实际就是通知方法(advice),这些通知注解中是可以使用切点表达式的,实际上这种注解的方式最终还是会被转换成advisor与MethodInterceptor(spring内置的环绕通知方式),转换的单位是以通知方法为单位进行转换的。

 

自动后置代理处理器

自动后置代理处理器(annotationAwareAspectJAutoProxyCreator):先去切点表达式中拿到这个要切的方法,然后去目标对象中寻找有没有与切点表达式对应的方法,如果可以匹配上那么就会为这个目标创建代理对象(自动创建),创建的代理对象会被放在单例池中,最终我们通过getBean获取的是代理对象,当你调用代理对象中的被增强的方法的时候,就会去找到每一个代理对象关联的那些advisor切面,然后再通过这个advisor切面去匹配这个切点表达式,如果匹配到了那就会调用相关通知的逻辑。

annotationAwareAspectJAutoProxyCreator这个类中重要的方法:

  • wrapIfNecessary:负责检查需不需要创建代理,比如切面和通知这种类型的肯定是不需要进行创建代理的,不然就会导致代理死循环了,还有就是没有匹配到切点表达式的方法肯定也是不需要创建代理的,这个wrapIfNecessary就是负责排除不需要创建代理的方法。

    • wrapIfNecessary中有三个重要的作用:

      • 检查类是不是基础类型,可以把切点,切面,通知等排除在外。

      • 根据你当前bean的类型去容器中寻找真正的切面类(advisor)

      • 通过代理工厂创建代理对象

总结:

  • 最基本的切面类是advisor,一个Aspecj切面根据内置方法不同对应一到多个Advisor。

  • 最基本的通知是MethodInterceptor,其他advice(比如前置,后置,环绕)最终都会适配为MethodInterceptor

  • 代理创建的方式

    • 实现了用户自定义的接口,采用jdk的动态代理。

    • 没有实现用户自定义接口,采用cjlib代理。

    • 如果设置了setProxyTargetClass(ture) ,统一采用cjlib代理。

  • 自动代理后置处理器:切面,切点,通知等不会被代理

  • annotationAwareAspectJAutoProxyCreator调用时机:创建阶段,依赖注入阶段,初始化阶段(主要)

循环依赖

set注入导致的循环依赖以及spring中的三级缓存

 

使用set注入的循环依赖可以依靠spring的三级缓存解决。

  • 一级缓存(singletonObjects):限制bean在beanFactory中只存在一份,即实现singleton scope。但是一级缓存是解决不了循环依赖问题的,所以spring引入了三级缓存(spring中叫三级缓存)。

  • 三级缓存(singletonFactories):存放工厂对象,发生循环依赖时,会调用工厂获取产品

从上面的图我们可以知道,一级缓存是解决不了循环依赖的!!!所以spring引入了一个三级缓存。

三级缓存解决循环依赖的原理:把初始化后的半成品对象马上注入到三级缓存中,这样注入的依赖B的初始化就可以直接从三级缓存中获取到半成品的A,从而完成B的初始化。当成品成品的bean完成构建放入到一级缓存后,三级缓存中半成品的bean就会被清理。

  • 二级缓存(earlySingletonObjcts):存放发生循环依赖的产品对象(可能是原始的bean,也可能是代理bean)

本来两级缓存就可以解决循环依赖,为什么spring设置了三级缓存?因为bean在完成初始化后可能是需要对其进行功能的增强,也就是说需要创建代理对象,最终也是需要把这个代理对象作为这个成品放入这个一级缓存的(因为以后你要使用增强后的bean那么你肯定是需要从一级缓存中拿到这个代理对象的),二级缓存会导致B在注入A的时候拿到的还是A的原始对象,那么此时A的增强功能是不能被B给调用的,这样就出现了问题。  

 我们可以从图中发现,之所以会出现这个原因,那是因为这个代理的创建时机太迟了,解决办法是让代理对象提前创建。而spring采用的是只有发生循环依赖的时候才会提前创建这个代理对象(spring并没有采用直接就创建代理对象),实现方式是就是再来一个缓存(earlySingletonObjcts)和 一个工厂对象(这个工厂对象的作用是判断有没有循环依赖,如果有循环依赖则提前创建代理返回,如果没有循环依赖,那么还是返回原始对象)

构造注入的循环依赖以及解决方案

用spring的三级缓存是解决不了构造注入导致的循环依赖的,因为使用构造注入的时候,需要提供相关的bean参数(有参构造),而此时bean都还没有进行实例化,也就是说缓存中并没有半初始化的bean,从而就会进入循环依赖。

方法一:通过提供代理对象来解决构造导致的循环依赖:

A创建的时候需要B才能继续执行A的构造,我们先给A提供一个假的B(b的代理对象,相当于推迟获取对象B),这样就可以让A进行构造以及后面的初始化,当B的构造方法也要执行的时候,此时A已经在一级缓存中了,B就可以顺利的从一级缓存中获取到A从而完成B的构造。

具体用代码怎么实现呢?

在A的构造方法中的参数B加一个@Lazy注解,这个注解的作用是在进行注入的时候是把B它的代理进行注入,并不会把真实的B进行注入

public class A{
    private B b;
    
    public A(@Lazy B b){  //把B的代理对象进行注入,相当于推迟获取目标对象B
        this.b = b;
    }
}

public class B{
    private A a;
    
    public B(A a){
        this.a = a;
    }
}

方法2:提供注入ObjectFactory来进行解决这个构造方法导致的循环依赖,原理是推迟获取对象B(优先使用这种方法)

public class A{
    private ObjectFactory<B> b;  //以后要使用B对象的话可以使用该工厂对象的getObject方法来间接的获取这个真实的B
    
    public A(ObjectFactory<B> b){  //注入工厂对象,不注入真实的B,这个时候只会把工厂注入,并不会调用B的构造,所以可以先完成了A的初始化,然后再把A存放到一级缓存中
        this.b = b;
    }
}

public class B{
    private A a;
    
    public B(A a){
        this.a = a;
    }
}

 

方法3:使用scope注解也可以解决,因为lazy注解解决构造循环依赖的原理是为B生成代理对象,scope注解中的proxyMode属性也是可以为目标对象生成代理对象,不过该注解的扫描方式只能提供配置文件来进行扫描或者是提供组件扫描的方式才可以使该注解生效。

(不推荐使用scope来解决这个构造产生的循环依赖,因为会产生额外的beandefination)

public class A{
    private B b;
    
    public A(B b){  //把B的代理对象进行注入,相当于推迟获取目标对象B
        this.b = b;
    }
}

@Scope(peoxyMode = ScopedProxyMode.TARGET_CLASS) //为B生成代理对象
@Component //为了进行组件扫描使scope注解生效
public class B{
    private A a;
    
    public B(A a){
        this.a = a;
    }
}

小结

单例set方法(包括成员变量)循环依赖,spring会利用三级缓存来解决,无需额外的配置

  • 一级缓存存放成品对象

  • spring中的二级缓存存放发生循环依赖的产品对象(可能是原始的bean,也可能是代理bean)

  • spring中的三级缓存

  • spring期望再初始化时创建代理,但是如果发生了循环依赖,会由工厂【提前】创建代理,后续初始化时就不必重复创建代理

  • 二级缓存的意义在于,如果提前创建了代理对象,在最后的阶段需要从二级缓存中获取此代理对象,作为最终的结果

构造方法及多例循环依赖解决的办法:

  • @Lazy注解

  • @scope注解

  • ObjectFactory等等

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值