背景
在工作过程中因为一些特殊的原因,想通过@Transactional和@Async注解来实现特殊的业务场景。不过在使用的过程中遇到了一些出乎意料的情况,也是因为对Spring源码框架的不熟悉,在经过几天的断点调试和csdn结合的方式终于梳理清楚遇到的问题的脉络,以及产生意料之外情况的原因。故在此做一个总结,以及分享一下学习到的相关知识点。
至于为什么要联合使用@Transactional和@Async这两个注解就不做说明了,可能按照正常的逻辑,这两个注解的应用场景应该是对立的,在一起使用可能存在逻辑上的冲突,不过我的使用场景也是特殊的应用场景,故在此不做深入分析探讨这两个注解应不应该联合使用的问题。
问题产生的环境
Springboot 2.5.2 + Shiro 1.4.0(安全框架)
抛出的问题一
在业务中联合使用@Transactional和@Async这两个注解,@Transactional标注的方法事务未生效,通过TransactionSynchronizationManager获取到的事务名为空,表明了根本不存在事务, 特别说明下标注的注解为@Transactional(propagation = Propagation.REQUIRES_NEW),应该不管在同步线程或者异步线程中都应该会去新建事务,是什么导致标注的事务注解未生效呢?然后在测试方法中去复现这种场景,但是这次成功了,在同时标注了@Transactional和@Async这两个注解的方法中,在异步线程中新建了事务,也是如预期所示。那为什么在业务中又没有成功呢?带着这个问题开始出发。。。
探究过程
在测试类中成功开启事务后,意外debug模式下看到了某些bean依赖的属性竟然是二次代理的一个结构(解释二次代理的结构:指一个被依赖的类是一个代理,然后这个代理代理的代理对象仍然是一个代理,相当于要调用到原始对象需要两次代理),在debug模式的调试下发现有两个类InfrastructureAdvisorAutoProxyCreator和DefaultAdvisorAutoProxyCreator对注入的bean进行了两次代理,InfrastructureAdvisorAutoProxyCreator是@EnableTransactionManagement注解导入的类,而DefaultAdvisorAutoProxyCreator是ShiroAnnotationProcessorAutoConfiguration配置类导入的类
这两个代理类都继承了AbstractAdvisorAutoProxyCreator这个抽象的AOP代理类,而且它的顶级接口InstantiationAwareBeanPostProcessor是一个beanPostProcessor,因此他有beanPostProcessor干预bean创建阶段的能力。
言归正传,就是因为项目中继承了shiro这个框架,导致项目中有多个AOP代理处理器,从而将一个代理类进行多次代理,正常情况下也就是这样。回到抛出的问题一,在业务中同时使用@Transactional和@Async这两个注解时,事务注解为什么没有生效,但是在测试方法中,事务又生效了呢?
还是在经过debug模式发现了问题所在,向业务中注入的依赖属性发现有的是原始对象(尽管对象上的方法标注了@Transactional),有的是代理对象(对象方法加了@Async注解的),事务未生效的原因很明显了,依赖的属性是原始对象,没有进行代理,也即我们添加的注解@Transactional未生效。
引出的问题二
对象依赖的属性注入的是原始未被代理的对象,也是某些对象方法上标注的@Transactional注解未生效的原因?
探究过程:
在debug模式下断点调试项目中整个bean创建的过程及顺序,发现了问题所在,即注入到Spring容器中匹配事务注解的通知BeanFactoryTransactionAttributeSourceAdvisor,因为依赖的关系在某些bean创建之后才能正常创建,导致了很多应该被代理的bean错过了代理时机。而依赖关系也是因为引入了shiro框架造成的,前面提到过Shiro是安全框架,有一些检验权限和安全的操作,故耦合了系统中的用户、角色等处理类,又因为Spring依赖机制,会先创建依赖的bean,故造成了大量的bean在这个依赖关系链中都创建完成,而造成了未完成事务的代理。
下面简述一下这个流程:
首先一些需要被代理的类,需要进入到自动代理创建器中判断自己是否需要被代理,上面也说过InfrastructureAdvisorAutoProxyCreator和DefaultAdvisorAutoProxyCreator都继承了AbstractAutoProxyCreator这个类,所以当获取某个bean的earlyBeanReference(Spring创建bean的一个中间过程,提前暴露bean的实例,以解决循环引用的问题),当调用这两个类时都会进入到wrapIfNecessary方法中
从这个方法一只走下去,首次会调用下面的方法去Spring容器中拿到所有的Advisor类型的通知者,这里回忆下事务的通知者BeanFactoryTransactionAttributeSourceAdvisor就是这个类型;对应的事务通知者也是这个时候首次注入到Spring容器中
问题的原因也是从这里开始,引入的Shiro也注入了一个AuthorizationAttributeSourceAdvisor的通知者,这个类会依赖一个安全管理器,这个安全管理器也是自己进行了重写,它又依赖了一些鉴权的bean,当然这些bean有的又关联了我们的业务,所以就是从这里开始依赖,进而还没有创建事务通知者,就已经完成了业务中的一些service类的创建,所以,也就解释了为什么有些事务注解没有被代理的原因。
如果我们项目里面设置的日志等级为Trace,日志中可以输出一些bean创建的过程中绕过了某些advisor,事务不生效的原因也是从这里跳过了事务通知者
当然,上面这个依赖的问题我们可以手动解决,比如使用@Lazy注解,让事务通知者先完成bean的创建,这样等事务创建好以后再去实例化bean,就可以完成bean的代理。也是从这里开始又引入了一个新的问题-Spring解决不了事务引起的循环依赖
引出的问题三
上面可以解决某些bean注入的属性不能被代理的情况,但是引入了新的问题,Spring竟然不能解决事务创建的代理引起的循环依赖,大家都知道,Spring是可以解决事务引起的循环依赖的问题。
探究过程:
还是原来的配方,还是熟悉的味道。使用debug模式调试,发现了问题所在。前面也提到过系统中会存在两个代理创建器InfrastructureAdvisorAutoProxyCreator和DefaultAdvisorAutoProxyCreator,在bean创建的过程中经过这两个代理创建器会让代理类变成一个两次代理类(前文解释过)。
若提前获取某个bean的earlyBeanReference时,会调用容器中的SmartInstantiationAwareBeanPostProcessor后置处理器循环处理bean的引用
因为循环依赖的关系在提前获取某个bean的earlyBeanReference时,当调用这两个类时都会进入到getEarlyBeanReference方法中,并且会把此时传入的bean对象放入到earlyProxyReferences这个缓存中。两个代理类的执行过程:首先InfrastructureAdvisorAutoProxyCreator会先调用进来,参数bean还是原始的对象,此时放入到earlyProxyReferences中,经过wrapIfNecessary方法后会产生一个代理对象;然后再调用DefaultAdvisorAutoProxyCreator,此时的参数bean已经是上次代理后的,故这个cacheKey(也即beanName)对应的value是这个代理对象,注意不是放入的原始对象(这里就是引起不能解决循环依赖的关键)
在bean创建的整个流程中,先实例化bean;再在singletonObjects中放入对应这个bean的早期引用;然后是注入依赖属性;接着初始化;初始化完以后需要检查反依赖的bean是不是最终版本的bean;校验通过后还要给bean注册销毁的方法等;最后就完成了bean的创建。不能解决循环依赖的场景就是发生在bean初始化以后的检查中。
下图中我们又看见了熟悉的代理创建器,对的,就是InfrastructureAdvisorAutoProxyCreator和DefaultAdvisorAutoProxyCreator都继承了这个类,所以当循环调用bean的后置处理器时,会进来两次
这里会从前面获取bean的earlyBeanReference时放入到earlyProxyReferences缓存中去取对应的cacheKey的值和当天bean是否相等。这里当InfrastructureAdvisorAutoProxyCreator先调用进来时,InfrastructureAdvisorAutoProxyCreator第一次存入也是原始对象,故这里两个值是相等的,故跳过代理,这里也是为什么原生Spring可以解决事务引起的循环依赖的原因;然后DefaultAdvisorAutoProxyCreator再调用进来,回想之前DefaultAdvisorAutoProxyCreator放入到earlyProxyReferences中的是由InfrastructureAdvisorAutoProxyCreator创建的代理,然后此时bean是一个原始bean,所以this.earlyProxyReferences.remove(cacheKey) != bean这个判断为true,故会再一次进入wrapIfNecessary方法中进行一次代理
在初始化方法执行完以后,会对当前创建的bean,以及当前bean被其他bean注入的版本进行检查,看是否是同一个实例,经过上面的判断我们知道在这里Spring会抛出BeanCurrentlyInCreationException,因为exposedObject == bean这个等式不成立,故也回答了不能解决循环依赖的原因
最后
到这里也梳理完了问题,以及问题产生的原因,但是在这里还没有给出问题解决得方案,因为我也还在寻求问题得答案。不过这里可以说下解决的思路:
1.引入的Shiro看有没有必要,如果不是必须就可以在启动类注解中排除ShiroAnnotationProcessorAutoConfiguration这个类。如果使用了Shiro的权限校验,则直接移除容器中的DefaultAdvisorAutoProxyCreator这个bean就会导致Shiro配置的权限验证失效,也就是对应的方法不会被对应的代理器代理;
2.使用@Lazy注解,自己手动去解决相关的循环依赖
3.在bean创建之前设置这个参数allowRawInjectionDespiteWrapping为true,这种方法不建议