Spring 循环依赖之AOP实现详情

前言:

我们接着上一篇文章继续往下看,首先看一下下面的例子,前面的两个serviceA和serviceB不变,我们添加一个BeanPostProcessor

@Component
public class MyPostProcessor implements BeanPostProcessor {
   @Override
   public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
       if (beanName.equals("serviceA")){
           System.out.println("create new ServiceA");
           return new ServiceA();
       }
       return bean;
   }
}

运行一下,结果报错了:

Exception in thread "main" org.springframework.beans.factory.BeanCurrentlyInCreationException: 
Error creating bean with name 'serviceA': Bean with name 'serviceA' 
has been injected into other beans [serviceB] 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.

在分析错误之前,我们再梳理一下正常循环依赖的过程:

  • 1、初始化原生对象serviceA,放入三级缓存
  • 2、serviceA填充属性,发现依赖serviceB,创建依赖对象
  • 3、创建serviceB,填充属性发现依赖serviceA,从三级缓存中找到填充
  • 4、执行serviceB的后置处理器和回调方法,放入单例池
  • 5、执行serviceA的后置处理器和回调方法,放入单例池

再回头看上面的错误,大意为在循环依赖中我们给serviceB注入了serviceA,但是注入之后我们又在后置处理器中对serviceA进行了包装,因此导致了serviceB中注入的和最后生成的serviceA不一致。

但是熟悉aop的同学应该知道,aop的底层也是利用后置处理器实现的啊,那么为什么aop就可以正常执行呢?我们添加一个切面横切serviceA的getServiceB方法:

@Component
@Aspect
public class MyAspect {
    @Around("execution(* com.hydra.service.ServiceA.getServiceB())")
    public void invoke(ProceedingJoinPoint pjp){
        try{
            System.out.println("execute aop around method");
            pjp.proceed();
        }catch (Throwable e){
            e.printStackTrace();
        }
    }
}

先不看运行结果,代码可以正常执行不出现异常,那么aop是怎么实现的呢?

前面的流程和不使用aop相同,我们运行到serviceB需要注入serviceA的地方,调用getSingleton方法从三级缓存中获取serviceA存储的singletonFactory,调用getEarlyBeanReference方法。在该方法中遍历执行SmartInstantiationAwareBeanPostProcessor后置处理器的getEarlyBeanReference方法:

看一下都有哪些类实现了这个方法:

在spring中,就是这个AbstractAutoProxyCreator负责实现了aop,进入getEarlyBeanReference方法:

public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
    //beanName
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    this.earlyProxyReferences.put(cacheKey, bean);
    //产生代理对象
    return wrapIfNecessary(bean, beanName, cacheKey);
}

earlyProxyReferences 是一个Map,用于缓存bean的原始对象,也就是执行aop之前的bean,非常重要,在后面还会用到这个Map:

Map<Object, Object> earlyProxyReferences = new ConcurrentHashMap<>(16);

记住下面这个wrapIfNecessary方法,它才是真正负责生成代理对象的方法:

上面首先解析并拿到所有的切面,调用createProxy方法创建代理对象并返回。然后回到getSingleton方法中,将serviceA加入二级缓存,并从三级缓存中移除掉。

可以看到,二级缓存中的serviceA已经是被cglib代理过的代理对象了,当然这时的serviceA还是没有属性值填充的。

那么这里又会有一个问题,我们之前讲过,在填充完属性后,会调用后置处理器中的方法,而这些方法都是基于原始对象的,而不是代理对象。

在前一篇文章中我们也讲过,在initializeBean方法中会执行后置处理器,并且正常情况下aop也是在这里完成的。那么我们就要面临一个问题,如果避免重复执行aop的过程。在initializeBean方法中:

if (mbd == null || !mbd.isSynthetic()) {
  wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

调用applyBeanPostProcessorsAfterInitialization,执行所有后置处理器的after方法:

执行AbstractAutoProxyCreatorpostProcessAfterInitialization方法:

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
  if (bean != null) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    if (this.earlyProxyReferences.remove(cacheKey) != bean) {
      return wrapIfNecessary(bean, beanName, cacheKey);
    }
  }
  return bean;
}

earlyProxyReferences 我们之前说过非常重要,它缓存了进行aop之前的原始对象,并且这里参数传入的Object也是原始对象。因此在这里执行remove操作的判断语句返回false,不会执行if中的语句,不会再执行一遍aop的过程。

回过头来再梳理一下,因为之前进行过循环依赖,所以提前执行了AbstractAutoProxyCreatorgetEarlyBeanReference方法,执行了aop的过程,在earlyProxyReferences中缓存了原生对象。因此在循环依赖的情况下,等式成立,直接返回。而在没有循环依赖的普通情况下,earlyProxyReferences执行remove返回为null,等式不成立,正常执行aop流程。

需要注意的是,这个方法中最终返回的还是原始对象,而不是aop后的代理对象。执行到这一步,我们先看一下嵌套的状态:

对外暴露的serviceA是原始对象,依赖的serviceB已经被注入了。而serviceB中依赖的serviceA是代理对象,并且这个代理对象依赖的serviceB还没有被注入。

向下执行:

再次通过getSingleton获取serviceA:

这次我们通过二级缓存就可以拿到之前经过aop的代理对象,因此不用找三级缓存直接返回这个代理对象,并最终把这个代理对象添加到一级缓存单例池中。

到这,我们对三级缓存的作用做一个总结:

  • 1、singletonObjects:单例池,缓存了经过完整生命周期的bean
  • 2、earlySingletonObjects:缓存了提前曝光的原始对象,注意这里存的还不是bean,这里存的对象经过了aop的代理,但是没有执行属性的填充以及后置处理器方法的执行
  • 3、singletonFactories:缓存的是ObjectFactory,主要用来去生成原始对象进行了aop之后得到的代理对象。在每个bean的生成过程中,都会提前在这里缓存一个工厂。如果没有出现循环依赖依赖这个bean,那么这个工厂不会起到作用,按照正常生命周期执行,执行完后直接把本bean放入一级缓存中。如果出现了循环依赖依赖了这个bean,没有aop的情况下直接返回原始对象,有aop的情况下返回代理对象。

全部创建流程结束,看一下结果:

我们发现,在生成的serviceA的cglib代理对象中,serviceB属性值并没有被填充,只有serviceB中serviceA的属性填充成功了。

可以看到如果使用cglib,在代理对象的target中会包裹一个原始对象,而原始对象的属性是被填充过的。

那么,如果不使用cglib代理,而使用jdk动态代理呢?我们对之前的代码进行一下改造,添加两个接口:

public interface IServiceA {
    public IServiceB getServiceB();
}
public interface IServiceB {
    public IServiceA getServiceA();
}

改造两个Service类:

@Component
public class ServiceA implements IServiceA{
    @Autowired
    private IServiceB serviceB;

    public IServiceB getServiceB() {
        System.out.println("get ServiceB");
        return this.serviceB;
    }
}
@Component
public class ServiceB implements IServiceB{
    @Autowired
    private IServiceA serviceA;

    public IServiceA getServiceA() {
        System.out.println("get ServiceA");
        return serviceA;
    }
}

执行结果:

看一下serviceA的详细信息:

同样也是在target中包裹了原生对象,并在原生对象中注入了serviceB的实例。

综上两种方法,可以看出在我们执行serviceA的getServiceB方法时,都无法正常获取到其bean对象,都会返回一个null值。那么如果非要直接获得这个serviceB应该怎么办呢?

我们可以通过反射的方式,先看cglib代理情况下:

ServiceA serviceA= (ServiceA) context.getBean("serviceA");
Field h = serviceA.getClass().getDeclaredField("CGLIB$CALLBACK_0");
h.setAccessible(true);
Object dynamicAdvisedInterceptor = h.get(serviceA);
Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
advised.setAccessible(true);
Object target = ((AdvisedSupport)advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
ServiceA serviceA1= (ServiceA) target;
System.out.println(serviceA1.getServiceB());

再看看jdk动态代理情况下:

IServiceA serviceA = (IServiceA) context.getBean("serviceA");
Field h=serviceA.getClass().getSuperclass().getDeclaredField("h");
h.setAccessible(true);
AopProxy aopProxy = (AopProxy) h.get(serviceA);
Field advised = aopProxy.getClass().getDeclaredField("advised");
advised.setAccessible(true);
Object target = ((AdvisedSupport)advised.get(aopProxy)).getTargetSource().getTarget();
ServiceA serviceA1= (ServiceA) target;
System.out.println(serviceA1.getServiceB());

执行结果都能获取到serviceB的实例:

对aop情况下的循环依赖进行一下总结:spring专门为了处理aop情况下的循环依赖提供了特殊的解决方案,但是不论是使用jdk动态代理还是cglib代理,都在代理对象的内部包裹了原始对象,在原始对象中才有依赖的属性。此外,如果我们使用了后置处理器对bean进行包装,循环依赖的问题还是不能解决的。

总结:

最后对本文的重点进行一下总结:

  • 1、spring通过借助三级缓存完成了循环依赖的实现,这个过程中要清楚三级缓存分别在什么场景下发挥了什么具体作用
  • 2、产生aop情况下,调用后置处理器并将生成的代理对象提前曝光,并通过额外的一个缓存避免重复执行aop
  • 3、二级缓存和三级缓存只有在产生循环依赖的情况下,才会真正起到作用
  • 4、此外,除去本文中提到的通过属性的方式注入依赖的情况外,大家可能会好奇如果使用构造函数能否实现循环依赖,结果是不可以的。具体的调用过程这里不再多说,有兴趣的同学可以自己再对照源码进行一下梳理。

到此这篇关于Spring 循环依赖之AOP实现详情的文章就介绍到这了,更多相关Spring 循环依赖 AOP 实现内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 教你Spring如何使用三级缓存解决循环依赖

    一级缓存存放实例化对象 .二级缓存存放已经在内存空间创建好但是还没有给属性赋值的对象.三级缓存存放对象工厂,用来创建提前暴露到bean的对象. @Service public class TestService1 { @Autowired private TestService2 testService2; public void test1() { } } @Service public class TestService2 { @Autowired private TestService1

  • SpringBoot通过AOP与注解实现入参校验详情

    目录 前言: 注解标记 通过AOP对方法进行增强 测试Get请求 测试POST请求 解决方法代码 再次测试POST请求 前言: 问题源头: 在日常的开发中,在Service层经常会用到对某一些必填参数进行是否存在的校验.比如我在写一个项目管理系统: 这种必填参数少一些还好,如果多一些的话光是if语句就要写一堆.像我这种有代码洁癖的人看着这一堆无用代码更是难受. 如何解决: 在Spring里面有一个非常好用的东西可以对方法进行增强,那就是AOP.AOP可以对方法进行增强,比如:我要校验参数是否存在

  • SpringBoot使用AOP实现统计全局接口访问次数详解

    目录 AOP是什么 AOP的作用和优势 常见的动态代理技术 AOP相关概念 实现 AOP是什么 AOP(Aspect Oriented Programming),也就是面向切面编程,是通过预编译方式和运行期间动态代理实现程序功能的传统已维护的一种技术. AOP的作用和优势 作用:在程序运行期间,在不修改源代码的情况下对某些方法进行功能增强 优势:减少重复代码,提高开发效率,并且便于维护 常见的动态代理技术 jdk代理:基于接口的动态代理技术 cglib代理:基于父类的动态代理技术 AOP相关概念

  • 关于SpringBoot禁止循环依赖解说

    前言: Spring的Bean管理,一直是整个体系中津津乐道的东西.尤其是Bean的循环依赖,更是很多面试官最喜欢考察的2B知识点之一. 但事实上,项目中存在Bean的循环依赖,是代码质量低下的表现.多数人寄希望于框架层来给擦屁股,造成了整个代码的设计越来越糟,最后用一些奇技淫巧来填补犯下的错误. 还好,SpringBoot终于受不了这种滥用,默认把循环依赖给禁用了! 从2.6版本开始,如果你的项目里还存在循环依赖,SpringBoot将拒绝启动! 验证代码小片段: 为了验证这个功能,我们只需要

  • SpringBoot2.6.x默认禁用循环依赖后的问题解决

    目录 一.序言 二.问题复原 1.代码说明 2.错误示例 三.问题解决 1.粗暴解决 2.优雅解决 四.小结 一.序言 SpringBoot 2.6.x不推荐使用循环依赖,这是一个好消息,SpringBoot从底层逐渐引导开发者书写规范的代码,同时也是个忧伤的消息,循环依赖的应用场景实在是太广泛了. 如果从低版本升级到2.6.x,那么很大概率遇到的第一个问题便是循环依赖问题. 二.问题复原 1.代码说明 下面风格的代码比较普遍:两个类都有调用对方方法的需求,因此很容易写成循环引用. @Servi

  • 一文搞懂Spring AOP的五大通知类型

    目录 一.通知类型 二.环境准备 添加AOP依赖 创建目标接口和实现类 创建通知类 创建Spring核心配置类 编写运行程序 三.添加通知 普通通知 环绕通知(重点) 一.通知类型 Advice 直译为通知,也有人翻译为 “增强处理”,共有 5 种类型,如下表所示. 通知类型 注解 说明 before(前置通知) @Before 通知方法在目标方法调用之前执行 after(后置通知) @After 通知方法在目标方法返回或异常后调用 after-returning(返回通知) @AfterRet

  • Spring源码解析之循环依赖的实现流程

    目录 前言 循环依赖实现流程 前言 上篇文章中我们分析完了Spring中Bean的实例化过程,但是没有对循环依赖的问题进行分析,这篇文章中我们来看一下spring是如何解决循环依赖的实现. 之前在讲spring的过程中,我们提到了一个spring的单例池singletonObjects,用于存放创建好的bean,也提到过这个Map也可以说是狭义上的spring容器. private final Map<String, Object> singletonObjects = new Concurr

  • 使用Spring AOP实现用户操作日志功能

    目录 我使用Spring AOP实现了用户操作日志功能 需求分析 功能实现 1. 需要一张记录日志的 Log 表 导出的 sql 如下: 2.我使用的是 Spring Boot 所以需要引入 spring aop 的 starter 3.Log 实体类 4.ILog 注解 5.切面类 LogAspect 总结 我使用Spring AOP实现了用户操作日志功能 今天答辩完了,复盘了一下系统,发现还是有一些东西值得拿出来和大家分享一下. 需求分析 系统需要对用户的操作进行记录,方便未来溯源 首先想到

  • Spring 循环依赖之AOP实现详情

    前言: 我们接着上一篇文章继续往下看,首先看一下下面的例子,前面的两个serviceA和serviceB不变,我们添加一个BeanPostProcessor: @Component public class MyPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansEx

  • Java中的Spring循环依赖详情

    目录 什么是循环依赖? 那么循环依赖是个问题吗? Bean的生命周期 三级缓存 解决循环依赖思路分析 Spring到底解决了哪种情况下的循环依赖 总结 什么是循环依赖? 很简单,就是A对象依赖了B对象,B对象依赖了A对象. 比如: 那么循环依赖是个问题吗? 如果不考虑Spring,循环依赖并不是问题,因为对象之间相互依赖是很正常的事情. 比如: 这样,A,B就依赖上了. 但是,在Spring中循环依赖就是一个问题了,为什么? 因为,在Spring中,一个对象并不是简单new出来了,而是会经过一系

  • Java Spring 循环依赖解析

    目录 1.常见问题 2.什么是循环依赖? 3.循环依赖说明 4.BeanCurrentlyInCreationException 5.依赖注入的两种方式 方式一:构造器方式注入依赖 方式二:以 set 方式注入依赖 6.Spring 三级缓存介绍和循环依赖解决过程 三级缓存介绍 实例化/初始化定义 三级缓存使用过程 A/B 两对象在三级缓冲的迁移说明 ObjectFactory 接口 DEBUG 断点调试 循环依赖解决 7.Spring 循环依赖总结 1.常见问题 你解释一下 spring 中的

  • Spring循环依赖的解决方法详解

    目录 什么是循环依赖: Spring实例Bean的本质 循环依赖主要场景 什么情况下循环依赖可以被解决 解决方式 说明:spring如何解决循环依赖,是面试中经常问到的题目,今天我们就来分享一下spring是如何解决循环依赖问题的. 什么是循环依赖: 我们先来看看官方文档的说法: 通俗来讲,就是A依赖B或者B依赖A,或者C依赖自己本身,或是三个以上,例如A依赖B,B依赖C,C又依赖A.如下图: Spring实例Bean的本质 Spring在实例化一个bean的时候,是首先递归的实例化其所依赖的所

  • Spring循环依赖的三种方式(推荐)

    引言:循环依赖就是N个类中循环嵌套引用,如果在日常开发中我们用new 对象的方式发生这种循环依赖的话程序会在运行时一直循环调用,直至内存溢出报错.下面说一下spring是如果解决循环依赖的. 第一种:构造器参数循环依赖 Spring容器会将每一个正在创建的Bean 标识符放在一个"当前创建Bean池"中,Bean标识符在创建过程中将一直保持 在这个池中,因此如果在创建Bean过程中发现自己已经在"当前创建Bean池"里时将抛出 BeanCurrentlyInCrea

  • 简单了解Spring循环依赖解决过程

    这篇文章主要介绍了简单了解Spring循环依赖解决过程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 前言 说起Spring中循环依赖的解决办法,相信很多园友们都或多或少的知道一些,但当真的要详细说明的时候,可能又没法一下将它讲清楚.本文就试着尽自己所能,对此做出一个较详细的解读.另,需注意一点,下文中会出现类的实例化跟类的初始化两个短语,为怕园友迷惑,事先声明一下,本文的实例化是指刚执行完构造器将一个对象new出来,但还未填充属性值的状态,而

  • Spring循环依赖的解决办法,你真的懂了吗

    介绍 先说一下什么是循环依赖,循坏依赖即循环引用,两个或多个bean相互引用,最终形成一个环.Spring在初始化A的时候需要注入B,而初始化B的时候需要注入A,在Spring启动后这2个Bean都要被初始化完成 Spring的循环依赖有两种场景 构造器的循环依赖 属性的循环依赖 构造器的循环依赖,可以在构造函数中使用@Lazy注解延迟加载.在注入依赖时,先注入代理对象,当首次使用时再创建对象完成注入 属性的循环依赖主要是通过3个map来解决的 构造器的循环依赖 @Component publi

  • Java Spring循环依赖原理与bean的生命周期图文案例详解

    前言 Spring是如何处理循环依赖的,又是怎么做到,互相注入对方的proxy bean而不是raw bean的?现在就分析一下 一.循环依赖是什么 Spring中放入两个Service,分别是C1和C2,然后C1和C2又互为对方的成员变量.这种情况C1和C2就可以说是相互循环依赖了 二.源码图解 1. bean的主要生命周期图解 上图是一个没有循坏依赖的bean的主要生命周期节点,下图的循坏依赖可以结合该图解一起看 2.循环依赖图解 可以看到里面有一个很重要的逻辑: 当一个bean经过所有的步

  • 浅入浅出的讲解Spring循环依赖问题

    目录 前言 概念 什么是循环依赖? 报错信息 通俗版理解 两人对峙 必须有一人妥协 Spring版理解 实例化和初始化什么区别? 三级缓存 创建过程(简易版) 创建过程(源码版) 最后 前言 最近有粉丝问到了循环依赖问题,以后再有人问你,拿这篇"吊打"他. 概念 什么是循环依赖? 多个bean之间相互依赖,形成了一个闭环.比如:A依赖于B.B依赖于C.C依赖于A. 通常来说,如果问Spring容器内部如何解决循环依赖,一定是指默认的单例Bean中,基于set方法构造注入的属性互相引用的

  • 一文搞懂Spring循环依赖的原理

    目录 简介 循环依赖实例 测试 简介 说明 本文用实例来介绍@Autowired解决循环依赖的原理.@Autowired是通过三级缓存来解决循环依赖的. 除了@Autoired,还有其他方案来解决循环依赖的,见:Spring循环依赖的解决方案详解 概述 @Autowired进行属性注入可以解决循环依赖.原理是:Spring控制了bean的生命周期,先实例化bean,后注入bean的属性.Spring中记录了正在创建中的bean(已经实例化但还没初始化完毕的bean),所以可以在注入属性时,从记录

随机推荐