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 注解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在 @Scheduled 注解添加 @Async 注解可以实现定时任务的异步执行。@Async 注解可以用于标记一个方法为异步方法,表示该方法将在一个独立的线程执行,而不阻塞当前线程。 下面是一个示例: ```java import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class MyScheduledTask { @Async @Scheduled(fixedRate = 5000) // 每隔5秒执行一次 public void myAsyncTask() { // 异步执行的任务逻辑 System.out.println("异步任务开始执行..."); // ... System.out.println("异步任务执行完成!"); } } ``` 在上述示例,我们在定时任务方法上同时添加了 @Async 和 @Scheduled 注解。@Scheduled 注解用于配置定时任务的执行频率,这里使用 fixedRate 表示每隔5秒执行一次。@Async 注解表示该方法将异步执行。 需要注意的是,为了使 @Async 注解生效,还需要在 Spring Boot 的主类上添加 @EnableAsync 注解。 ```java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication @EnableAsync public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } } ``` 这样配置之后,定时任务将在独立的线程异步执行,不阻塞当前线程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

魔道不误砍柴功

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值