【追根究底】 为什么@Transactional注解失效了?

springboot 专栏收录该内容
13 篇文章 2 订阅

==> 学习汇总(持续更新)
==> 从零搭建后端基础设施系列(一)-- 背景介绍


前言:这一篇文章通过分析根源,基本可以解决所有事务失效的情况了,但是因为spring掌握的有限,所以写的时候可能会有些地方描述得不是很好!大家可以指出我来改进!


  • 新手疑问之为什么我已经加上了@Transactional注解,还是失效呢???
    这个先从表面上判断(其根本原因下面讲,新手可能较难理解),可以参考细说@Transactional用法及原理
    ,简单了解失效的表面原因。

  • 老鸟致命疑问之为什么我已经加上了@Transactional注解,并且事务确认已经开启,最后已经生成代理类调用了,它还是失效???
    这个问题可能不常见,一旦遇到,那么除非是spring高高手,对源码熟知又熟,才可以瞬间解之,否则只能像我这种菜鸟,慢慢的跟着源码一路找到底才可能找到一丝光明。
    言归正传,我先来一个上述问题的例子。因为不好拿实际项目中的代码进行演示,所以我将问题抽出来复现一下即可。新建一个项目,按照细说@Transactional用法及原最后小白总结的来即可。最后如图所示
    在这里插入图片描述

    • RecordPointCut
      @Aspect
      @Component
      public class RecordPointCut {
      	@Pointcut("execution(public * com.acme.transactional.demo.service.*Impl.*(..))")
          public void myAnnotationPointcut(){
      
          }
      
          @Before("myAnnotationPointcut()")
          public void before(JoinPoint joinPoint){
              System.out.println(joinPoint.getTarget() + " begin:" + System.currentTimeMillis());
          }
      
          @After("myAnnotationPointcut()")
          public void after(JoinPoint joinPoint){
              System.out.println(joinPoint.getTarget() + " end:" + System.currentTimeMillis());
          }
      }
      
    • DatasourceConfig
       @Configuration
      public class DatasourceConfig {
       	@Bean
      	public DataSource dataSource() {
      		DriverManagerDataSource dataSource = new DriverManagerDataSource("");
      		dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
      		return dataSource;
      	}
      }
      
    • MyConfig
      @Configuration
      public class MyConfig {
          /*
          * 看着名字就知道,其它服务的bean,相当于我这个服务使用到了其它服务的bean
          * @Qualifier作用是指定注入哪个bean,这里生成OtherServerBean的时候,就会去创建HelloImpl
          * */
          @Bean
          OtherServerBean otherServerBean(@Qualifier("helloImpl")HelloImpl hello){
              System.out.println(hello);
              return new OtherServerBean();
          }
      }
      
    • Curd
      @Component
      public class Curd {
          public void add(){
              //数据库插入操作
          }
      }
      
    • OtherServerBean
      /*
      * 实现了FactoryBean接口,算是一个特殊的bean
      * */
      public class OtherServerBean implements FactoryBean<Object> {
          @Override
          public OtherServerBean getObject() throws Exception {
              return new OtherServerBean();
          }
      
          @Override
          public Class<OtherServerBean> getObjectType() {
              return OtherServerBean.class;
          }
      }
      
    • HelloImpl
      @Component
      public class HelloImpl{
      
          @Autowired
          private Curd curd;
          
          @Transactional
          public void sayHello(){
              curd.add();
          }
      }
      
    • 测试
      @SpringBootTest
      class DemoApplicationTests {
      	@Autowired
      	HelloImpl hello;
      	@Test
      	void contextLoads() {
      		hello.sayHello();
      	}
      }
      

    先说接下来会发生的现象,hello是一个代理类,但是事务并不生效(没有事务拦截器,可在源码中看到)
    接下来运行代码,如图所示,hello是一个代理类
    在这里插入图片描述
    再单步调试进去,红圈的地方,那三个拦截器,没有一个是事务拦截器的,这就可以反证出开头的那个现象,加了事务注解,是代理类,但是还是没有生效。
    在这里插入图片描述


    现在我们来找原因,首先,我的思路是,为什么hello这个bean没有事务拦截器?拦截器应该说明时候被加进去?是不是bean生成的时候?……(经过我长时间的脑暴+试验,终于找出一条路来,这里我直接说结论,因为路是自己走出来的,很难原样的告诉别人该怎么走)

    • 找到拦截器被加进去的地方
      寻找路线(中间有些会忽略,不然就太长了)
      SpringApplication.run(DemoApplication.class, args) -> refreshContext -> refresh -> finishBeanFactoryInitialization -> preInstantiateSingletons
      好,第一阶段的路线已经找到了,如图,像上面那行注释所说实例化所有非懒加载的bean。
      在这里插入图片描述
      第二阶段,走起,当实例化到demoApplication的时候,路线是这样的
      else中的getBean -> doGetBean -> mbd.isSingleton()分支的createBean ->resolveBeforeInstantiation->applyBeanPostProcessorsBeforeInstantiation->postProcessBeforeInstantiation(当ibp的类型为AnnotationAwareAspectJAutoProxyCreator时单步进去)->shouldSkip->findCandidateAdvisors->findAdvisorBeans
      好,到终点了,我们终于找到增强器(这里还只是增强器,拦截器还要在外面被生成,但是我们的目的已经达到了)被加进去的地方了。如图,看向标1和2的地方,beanNamesForTypeIncludingAncestors这个方法,是去寻找spring工厂中是否有增强器,果不其然,被找到了一个事务增强器,标3所示。
      在这里插入图片描述
      接着往下走,可以看到标1的地方是判断这个bean是否正在创建中,如果是的话,是不会走到标2这个地方的。从图中可以看出,这个bean还没开始创建,所以用beanFactory.getBean去创建它
      在这里插入图片描述

    • 拦截器创建的过程?
      接下来,我们要找出拦截器的创建过程,第一阶段路线
      beanFactory.getBean -> doGetBean -> createBean -> doCreateBean -> createBeanInstance -> instantiateUsingFactoryMethod
      好,第一阶段就走完了,如图,因为org.springframework.transaction.config.internalTransactionAdvisor有工厂方法,所以使用instantiateUsingFactoryMethod这个去实例化它,标2的地方是去创建org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration这个工厂类去了。
      在这里插入图片描述
      创建ProxyTransactionManagementConfiguration的逻辑和之前demoApplication的是一样的,会进入到resolveBeforeInstantiation中,然后试图去找增强器,发现它正在创建中(可不是嘛),直接就返回了,相当于没有找到增强器。
      在这里插入图片描述
      第二阶段,现在时间紧迫,直接开挂,到另一个地方吧,路线如下
      createBeanInstance -> populateBean -> ibp.postProcessProperties(这里需要找到正确的入口,当ibp为AutowiredAnnotationBeanPostProcessor时)->inject->inject->beanFactory.resolveDependency->doResolveDependency -> resolveMultipleBeans -> findAutowireCandidates(else的Collection.class分支)->beanNamesForTypeIncludingAncestors -> getBeanNamesForType -> doGetBeanNamesForType -> isTypeMatch -> getTypeForFactoryBean(在FactoryBean.class.isAssignableFrom(predictedType)下)
      好,路线到这里就结束了,原因我们也快找出来了,我们直接来看看
      getTypeForFactoryBean这个方法,看之前,我们要知道,为什么会进入到这里来,如图,可以看到,居然是otherServerBean这个bean,为什么呢?因为它是一个工厂bean,我们再来看看它的作用是什么,单步进去。
      在这里插入图片描述
      我们可以看到,如果myConfig这个类存在(因为otherServerBean这个是在它下面管理的bean),那么就尝试根据类型去获取otherServerBean,但是发现没有匹配的类型,为什么呢?我们回过头来看,otherServerBean的定义,FactoryBean是Object类型,但是getObjectType返回的却是OtherServerBean类型,所以就匹配不上了。

      /*
      * 实现了FactoryBean接口,算是一个特殊的bean
      * */
      public class OtherServerBean implements FactoryBean<Object> {
          @Override
          public OtherServerBean getObject() throws Exception {
              return new OtherServerBean();
          }
      
          @Override
          public Class<OtherServerBean> getObjectType() {
              return OtherServerBean.class;
          }
      }
      

      在这里插入图片描述

      那么匹配不上会怎么办?继续往下看,因为是单例,所以调的是getSingletonFactoryBeanForTypeCheck这个方法。
      在这里插入图片描述
      它里面会调用createBeanInstance去实例化这个bean,为什么要这样?因为它需要实例化出来才能判断它是啥类型呀。所以问题就在这里。
      在这里插入图片描述

      这里先小结一下,再看最后的结果。因为otherServerBean的创建出了问题

      • otherServerBeanFactoryBean类型是Object的,getObjectType又返回的是OtherServerBean,导致不能直接拿到getObject中的new OtherServerBean(),所以需要去实例化OtherServerBean出来才能去判断类型。
      • 实例化OtherServerBean又会去找MyConfig中注解为@BeanotherServerBean,这时候@Qualifier("helloImpl")HelloImpl hello也被实例化出来了,最终结果就是,hello先生成了,增强器还没创建出来,自然就生成不了拦截器,也就少了这个事务拦截器。

      如图所示,走到这一步后,会去尝试为这个bean获取增强器
      在这里插入图片描述
      但是此时事务增强器还正在创建
      在这里插入图片描述
      所以最后肯定是拿不到它的
      在这里插入图片描述
      最后回到createBean中的doCreateBean,可以看到hello先于org.springframework.transaction.config.internalTransactionAdvisor创建了。
      在这里插入图片描述
      在这里插入图片描述


  • 重要细节分析
    • 为什么preInstantiateSingletons 中,到了demoApplication的加载才开始去创建internalTransactionAdvisor
      有这疑问的,应该都没有实际运行这段代码,跟着调试进去看,那现在我就演示一下,如图,就拿第一个bean来说
      在这里插入图片描述
      这里可以看到,它之前已经被加载过了!所以直接从缓存中拿到,就不会进入到else分支进行bean的创建了!所以也就不会再到internalTransactionAdvisor这一步,同理,demoApplication前面的bean都是这样。
      在这里插入图片描述
    • 为什么会在postProcessBeforeInstantiation 中的shouldSkip去实例化internalTransactionAdvisor
      说到这里,确实是一个坑,postProcessBeforeInstantiation这个方法相当于是bean实例化前,判断一下是否需要走代理?然后它会判断,isInfrastructureClass(beanClass)是否是基础设施类(我直译过来的),判断代码如下,简单讲,如果是这三个类的子类,那么就算是。
      protected boolean isInfrastructureClass(Class<?> beanClass) {
      	boolean retVal = Advice.class.isAssignableFrom(beanClass) ||
      			Pointcut.class.isAssignableFrom(beanClass) ||
      			Advisor.class.isAssignableFrom(beanClass) ||
      			AopInfrastructureBean.class.isAssignableFrom(beanClass);
      	if (retVal && logger.isTraceEnabled()) {
      		logger.trace("Did not attempt to auto-proxy infrastructure class [" + beanClass.getName() + "]");
      	}
      	return retVal;
      }	
      
      然后判断是否应该跳过,代码如下,先去找到所有的增强器,然后再一一判断是否是AspectJPointcutAdvisor类型的,如果有的话就返回true,就可以跳过,不代理了。这里有一个坑,作者也注释了TODO,考虑是否用缓存优化,因为现在每次调用shouldSkip都会去调用findCandidateAdvisors,它去找增强器的时候,如果找到了bean的定义,那么就会去创建它,这一步其实可以用缓存代替,只需要走一次findCandidateAdvisors就行。所以当创建demoApplication的时候,会走到这里,然后找到internalTransactionAdvisor,并创建它,后来又导致一系列的递归创建。
      protected boolean shouldSkip(Class<?> beanClass, String beanName) {
      	// TODO: Consider optimization by caching the list of the aspect names
      	List<Advisor> candidateAdvisors = findCandidateAdvisors();
      	for (Advisor advisor : candidateAdvisors) {
      		if (advisor instanceof AspectJPointcutAdvisor &&
      				((AspectJPointcutAdvisor) advisor).getAspectName().equals(beanName)) {
      			return true;
      		}
      	}
      	return super.shouldSkip(beanClass, beanName);
      }
      
    • 创建ProxyTransactionManagementConfiguration的时候为什么会创建OtherServerBean
      是的,这两个看起牛马不相及,可是他们就是有关系了!让我们来看看这是怎么回事,从doCreateBeanpopulateBean说起,populateBean的作用就是为bean填充属性,比如A中有b、c属性,那么它就会去创建b、c然后填充进去。ProxyTransactionManagementConfiguration的父类以及自身都有需要填充的属性,如图,先看看父类的,因为肯定会先填充父类的属性。
      在这里插入图片描述
      再看看调试的信息,先为setConfigurers这个方法注入需要的bean
      在这里插入图片描述
      去找到适合这个类型的bean(根据类型查找)
      在这里插入图片描述
      因为毕竟深入,所以我直接贴出最后一层的截图(一步步跟进去即可找到),看方法名就知道,根据type获取bean,继续进去看看
      在这里插入图片描述
      重点来了,当判断到otherServerBean的时候,调用isTypeMatch的时候,问题来了,因为它是一个工厂bean,所以会走到一个比较特殊的地方。
      在这里插入图片描述
      没错,就是这里,如果是工厂bean,那么会调用getTypeForFactoryBean去拿到这个bean的类型。
      在这里插入图片描述
      再深入到里面,可以看到,拿到的getObjectType方法的返回类型是OtherServerBean,但是FactoryBean的类型却是Object
      在这里插入图片描述
      所以类型不匹配,找不到。如果找到的话,就直接返回,不会去实例化otherServerBean
      在这里插入图片描述
      接着就会去实例化otherServerBean这个bean,看看是什么类型,接下来的就没啥可说的了,
      在这里插入图片描述

  • 解决办法
    • 懒加载
      很好理解,既然增强器还没创建出来,我为什么要急着先加载呢?所以在所有使用到HelloImpl的地方,加上@Lazy
      @Bean
      OtherServerBean otherServerBean(@Lazy@Qualifier("helloImpl")HelloImpl hello){
          System.out.println(hello);
          return new OtherServerBean();
      }
      
      但是,这个例子不适用,为什么呢?可以看到,加上了@Lazy,但是当实例化otherServerBean的时候,helloImpl还是被实例化出来了。@Lazy有这么一条规则,如果标记为懒加载的类,需要注入到其它非懒加载的类的时候,懒加载失效!
      在这里插入图片描述
      所以,如果代码改成这样,就可以使用懒加载的方式
      在这里插入图片描述
      @Bean
      OtherServerBean otherServerBean(@Qualifier("THelloImpl")THelloImpl hello){
          System.out.println(hello);
          return new OtherServerBean();
      }
      
      这样,相当于THelloImpl被加载了,但是它的属性HelloImpl却可以懒加载了。
    • 修改OtherServerBeanFactoryBean类型
      这个方法可行性不高,原因是,如果这个是第三方bean,那么你修改不了。但是如果是自己写的话,就很好办了。直接修改类型即可。
      	public class OtherServerBean implements FactoryBean<OtherServerBean> {
          @Override
          public OtherServerBean getObject() throws Exception {
              return new OtherServerBean();
          }
      
          @Override
          public Class<OtherServerBean> getObjectType() {
              return OtherServerBean.class;
          }
      }
      
    • 最直接能避免这种情况的,就是不要使用注入的方式,直接new出对象来
      前提是不需要全局性,也就是单不单例的无所谓那种。但是很明显,这里不行,因为THelloImpl里面用到了spring的自动注入,所以THelloImpl也必须要由spring管理。
      @Bean
      OtherServerBean otherServerBean(){
          System.out.println(new THelloImpl());
          return new OtherServerBean();
      }
      

  • 总结
    • @Transactional失效的根本原因是事务增强器没有被加入到某个bean中,造成这种原因有两种,第一是根本找不到事务增强器,第二是某个bean先于事务增强器加载了,导致事务增强器没被加入。
    • 从表面上看,虽然它是一个代理类,但是里面却没有事务拦截器,有的是其它拦截器,所以当然会失效。
    • FactoryBean的用法,需要注意,<T>泛型参数不要设置为Object,否则就会造成类型检查的时候不匹配,转而去实例化该类获取类型,这又会导致一系列的连锁反应。
    • @Lazy标记的类,需要注入到其它非懒加载的类,那么就会失效。这个很容易理解,本来懒加载就是用的是才会加载,那现在别人需要用到你了,当然需要加载了。
    • 使用到事务注解的类,最好是放在最里层,外面包一层,用@Lazy标记,而且不要在其它地方再直接使用它。例如,A中的方法使用了事务,那么可以用B将A包起来,A在B中标记为懒加载,之后其它地方仅仅引用B,不要直接引用A,就可以避免项目中误使用导致的事务失效。
  • 2
    点赞
  • 0
    评论
  • 5
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:Age of Ai 设计师:meimeiellie 返回首页

打赏作者

_acme_

试着玩,各位看官随意

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值