• Spring 中自定义注解 @DBMasterAnno 和 @Async 注解在循环依赖场景下使用出现问题


    问题:Spring 中单例的循环依赖按理可以的,但是为什么在同时使用自定义注解 @DBMasterAnno 和 @Async 注解的时候,就会出现异常呢?

    抛出的异常如下所示:

    在这里插入图片描述

    下面开始追溯一下原因到底是哪里导致出错的呢?

    使用示例,先制造出一个单例的循环依赖环境,直接使用 @Autowired 注入自己即可,按理这个循环依赖是没问题的,Spring 是支持循环依赖问题的,但是由于这里加了两个切面 @DBMasterAnno 和 @Async 进来,这里就直接报错了。

    示例代码如下:

    在这里插入图片描述

    那么这里涉及到了依赖注入问题,就得从 Spring 的创建 bean 流程开始分析:

    首先 AopProxyOrderServiceImpl 开始调用 getBean() 方法,在 getBean() 方法中有个很重要的步骤,就是每个单例创建的时候都会先把自己放在一个三级缓存中,这个三级缓存是专门用来存半成品的实例 bean 或者是代理对象,源码如下:

    在这里插入图片描述

    进入到 addSingletonFactory() 方法内部,如下:

    在这里插入图片描述
    这里把把 singtletonFactory 工厂对象放进了三级缓存中,注意哦这是放的一个 singtletonFactory 对象工厂,这个对象工厂产生的 bean 可以有很多后置处理器生成。

    然后再看 addSingletonFactory(beanName,()->getEarlyBeanReference(xx)) 方法的第二个参数里面,是一个 Lambada 函数,进入该 getEarlyBeanReference() 方法,源码如下:

    在这里插入图片描述

    发现是一个 for 循环,这里我们关注 AbstractAutoProxyCreator 子类,为什么关注这个呢? 因为我们这里通过注解 @EnableAspectJAutoProxy 开启了切面功能,Spring 的 AOP 切面就是这个类帮我们实现的,所以不看它看谁。

    在这里插入图片描述

    进入这个类查看这里有两行非常非常重要的代码:

    在这里插入图片描述

    第一行可以看出 Spring 是将原始半成品 bean 放入到了 earlyProxyReferences 缓存保存起来的,记住这个缓存保存了原始半成品 bean

    第二行主要是判断需不需要创建代理对象,怎么判断呢?主要是你的 @ComponentScan 配置的包路径下面是否存子 @Transactional、@Aspect、自定义注解、@Async、@Cacheable 等有标识性的注解存在,如果有就会创建代理对象,源码如下:

    在这里插入图片描述

    因为我们通过注解 @EnableAspectJAutoProxy 开启了切面功能,所以这里就会去创建代理对象 $Proxy23@2054 ,然后返回源码查看代理类 hashCode() 如下所示:

    在这里插入图片描述

    再次声明一下此时 earlyProxyReferences 缓存的是**【原始】【原始】【原始】的半成品 bean 对象**,而这里 return 回去的是一个 代理对象。分析完第二个 Lambada 表达式方法,我们知道这里会帮我们创建一个代理对象,并且放入三级缓存,但是注意现在只是我们分析的过程,现在还并没有人去调用 getObject() 方法,也就不会触发这个 Lambada 函数去创建生成代理对象,只是说三级缓存里面暂时存放的是一个对象工厂,可以由这个工厂创建代理对象而已,注意这里埋了个点哦

    接着往下执行,开始给 @Autowired 修饰的属性赋值,代码如下:

    在这里插入图片描述

    调用 populateBean() 方法开始属性赋值,读过 Spring 源码的都知道这里会去触发属性的 getBean() 操作,源码步骤如下:

    在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    此时是 属性 AopProxyOrderService 触发了 getBean() 操作,那么又会开始回到 Spring 开始创建 bean 流程,源码步骤如下:

    在这里插入图片描述在这里插入图片描述在这里插入图片描述

    然后进入到大家最熟悉不过的三级缓存取值的步骤,注意现在过来的是属性触发的 getBean() 操作,然后先从一级缓存中查看是否有值,答案肯定是没有的,一级缓存中只有完成了 bean 整个过程才会有的,然后再查二级缓存中是否有值,答案肯定也是没有的,然后查询三级缓存,这里发现是有数据的,而且是一个 工厂对象,因为第一次过来的 AopProxyOrderServiceImpl 调用 getBean() 往三级缓存中保存了一个 工厂对象

    此时会在这里会调用工厂对象的 getObject() 方法,那么此时就会触发上面埋点地方说的 Lambada 函数开始执行,上面已经分析过了 Lambada 函数里面会给先把我们的原始 bean 对象放在一个 earlyProxyReferences 缓存中,然后在返回一个代理对象

    在这里插入图片描述

    属性调用 getBean() 方法从三级缓存中获取到了代理对象并返回,并且把三级缓存中的代理对象转存到二级缓存中,至此属性赋值的 getBean() 流程结束,至此属性已经有值了就是这个代理对象,然后再回到第一次进来的 bean 的调用处,会继续往下执行,源码如下:

    在这里插入图片描述

    然后代码继续往下执行,执行 initializeBean() 方法,源码如下:

    在这里插入图片描述
    在这里插入图片描述

    又开始调用后置处理器逻辑,先看下支持自定义注解的后置处理器中是怎么样处理的吧? 源码如下:

    在这里插入图片描述

    看到一个非常关键的 earlyProxyReferences 缓存,注意这个缓存在上面已经特别强调了里面存放的是第一次过来的 【原始】【原始】【原始】半成品 bean,注意 remove() 会把移除的对象返回哦,刚好这里返回出来的 bean 就是和 bean 相等的,但是判断条件式 != ,所以这里不成立,就不会再次进去创建代理对象了,因为这个代理对象已经被这个 AbstractAutoProxyCreator 类创建过了,所以不能再去创建。

    然后执行完返回的还是原始的 bean,回到循环调用处,源码如下:

    在这里插入图片描述

    current != null 所以不会跳出 for 循环,继续下一次 for 循环,那么因为我们这里还开启了 @Async 的功能,而且恰好,对这个注解的支持就是在这里的调用后置处理器处理的,进入到 @Async 的关键类 AbstractAdvisingBeanPostProcessor ,源码如下:

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    然后代码继续执行,返回到调用到上一层调用处,源码如下:

    在这里插入图片描述

    注意返回的又是一个代理对象(前面也有一个代理,而且在二级缓存中保存着),然后代码继续往下执行,先去缓存中取值,注意此时缓存中只有二级缓存中有一个代理对象,注意此时这俩个代理对象可不是一样的哦,取出来之后赋值到变量 earlySingletonReference 保存

    在这里插入图片描述

    这里有一个非常非常非常关键的判断条件 exposedObject == bean,exposedObject 是在后置处理器中生成的一个代理对象,而 bean 是最原始对象,两个相等吗?答案肯定是不相等的,所以会执行 else if 逻辑。

    走进 else if 逻辑如下所示,这个变量默认是 allowRawInjectionDespiteWrapping = false 的,并且有依赖关系,并且依赖的还是自个,所以这个判断条件会直接走进去。

    在这里插入图片描述
    在这里插入图片描述

    最终这个 actualDependentBeans 集合里面会添加值,如果有值,Spring 直接会抛出异常,其实思考下,肯定会出异常的,实际上一个类存在两个代理对象,不合理,你到底用哪个代理类呢?所以 Spring 这里直接给拦截处理了,而且这个异常就是文章开头的异常信息。

    在这里插入图片描述

    至此相信已经能够明白为什么 @Async 和其他切面注解不能共存的原因了吧。主要是 @Async 的接口不按套路出牌,不去实现 AbstractAutoProxyCreator 公共创建代理类,这个类人家加了缓存,能够保证单例下不会重复去创建代理类,@Async 注解的支持中可没有做这个判断,直接上来判断有没有开启 @Async 功能,就直接给我去重新创建了一个代理类,至此 Spring 容器中出现了同一类有两个代理类,从而导致报错。

    不过有办法解决,出错主要是因为 actualDependentBeans 集合有值,所以抛出了异常,那么认为控制它不要让它添加值不就不会出错了嘛,可以把这个开关设置成 true 即可,那怎么设置呢?

    在这里插入图片描述

    可以通过 BeanFactoryPostProcessor 工厂后置器修改工厂中的属性值,代码如下:

    在这里插入图片描述

    然后重新启动 Spring 发现是可以成功的,此时容器中存在两个代理对象,如下所示:

    在这里插入图片描述

    进入 createOrder() 方法,观察使用注解 @Autowired 注入的对象,代码如下:

    在这里插入图片描述

    两个代理对象看 hashCode() 明显是不同的,容器中存放的是第一个代理对象 $Proxy23@2052,@Autowired 注入的是第二个代理对象 $Proxy23@2282,所以 createOrder() 虽然配置了两个切面功能,但是此时只会生效一个,哪个代理对象调用这个方法就生效哪个代理对象的切面增强功能。

    验证如下:两个注解同时配置上去,然后再各自的切面逻辑中加上输出语句

    在这里插入图片描述在这里插入图片描述在这里插入图片描述

    然后启动测试类,如下:

    在这里插入图片描述

    运行结果如下所示:配置了两个切面,但是只生效了一个,因为是这个 @Async 生成的代理调用的而已

    在这里插入图片描述

    把 @Async 注解注释一下,在输出一次结果就会调用自定义注解切面了

    在这里插入图片描述

    运行结果如下:可以发现自定义注解的切面增强被调用了

    如果想要让两个切面都生效,这里有两种解决方案可以参考系(经供参考):

    方法一: 可以保证不存在循环依赖的问题

    在这里插入图片描述

    输出结果如下所示:两个切面功能都已经生效了,因为不存在循环依赖问题,现在容器中就只存着一个代理对象,两个切面功能都是同一个代理对象去增强的。

    在这里插入图片描述

    如果想要在业务方法中拿到代理对象,可以通过 AopContext.getCurrentProxy() 方法获取,但是使用这个的前提你必须要在注解上开启这个功能,如下配置:

    在这里插入图片描述

    为什么要开启呢?从源码中可以看到只有为 true 的时候才会把代理对象保存在 ThreadLocal 中,注意 ThreadLocal 只有在同一个线程过程中才能够获取到值,不同线程获取不到。

    在这里插入图片描述

    然后就可以通过下面获取到代理对象

    在这里插入图片描述

    但是发现这里会报异常,如下:

    在这里插入图片描述

    追溯到源码如下:

    在这里插入图片描述

    为什么不到 ThreadLocal 中的值,因为 @Async 注解会开启新的新的线程去执行业务逻辑,所以此时不在不在同一个线程中,自然而然获取不到 ThreadLocal 中的值,所以如果有 @Async 注解的时候不要使用 ThreadLocal,现在暂时没有想到好的解决办法能够让 @Async 注解和其他注解共存并且还要获取到代理对象的方法,如果你们有评论区见。

    方法二: 就是不要使用 @Async 注解,可以把 @Async 的功能放到业务中自己控制去执行,比如事件发送等等。

    总结:

    就是一句话,@Async 自己开了小灶,自己管理了生成代理对象的过程,在过程中没有判断代理是否生成过了而从心自己 new 了新的代理对象。如果和其他注解一样 @Transactional、@Aspec、自定义注解等等统一继承 AbstractAutoProxyCreator 接口多好,就不会存在这种问题了。

    所以 @Transactional、@Aspec、自定义注解等这几种注解就可以放在一起执行,就算是存在循环依赖也是没有问题的。验证截图如下:

    在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    两个切面功能都已经生效了,并且代理对象是同一个,都是 $Proxy27@2155 。所以慎用 @Async 注解。

  • 相关阅读:
    两个移相算法
    【探索Linux】文件描述符 | 重定向 | 基础IO —— 强大的命令行工具 P.12
    ubuntu20.04配置vscode
    关于模型融合Stacking的一些改进思路
    【逆向】03-20扩大节 c++代码完成 ;类当作函数参数却调用了析构函数疑难分析
    TFRecord的Shuffle、划分和读取
    rust入门
    SpringBoot业务开发 05、SpringBoot集成JSR303实现参数校验+全局异常捕捉
    Javascript知识【jQuery样式操作&案例:jQuery隔行换色】
    《羊了个羊》一夜爆红?产品运营带来的巨大红利
  • 原文地址:https://blog.csdn.net/qq_35971258/article/details/126471663