学个Spring吧[7]-调用同类方法事务不生效问题探索

在学习Spring事务这节的时候,遇到这样一个问题:调用同类方法事务不生效!具体问题描述如下:
测试的主要代码——

public interface FooService {
    void insertThenRollback() throws RollBackException;
    void invokeInsertThenRollback() throws RollBackException;
}
@Component
public class FooServiceImpl implements FooService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = RollBackException.class)
    public void insertThenRollback() throws RollBackException {
        jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('BBB')");
        throw new RollBackException();
    }
    // 注意! 这个方法没有申明事务!
    public void invokeInsertThenRollback() throws RollBackException {
        this.insertThenRollback();
    }
}
@SpringBootApplication
@EnableTransactionManagement(mode = AdviceMode.PROXY)
@Slf4j
public class XiaoLearnSpringApplication implements CommandLineRunner {

   @Autowired
   private FooService fooService;

   @Autowired
   private JdbcTemplate jdbcTemplate;

   public static void main(String[] args) {
      SpringApplication.run(XiaoLearnSpringApplication.class, args);
   }

   @Override
   public void run(String... args) throws Exception {
      try {
         fooService. insertThenRollback ();
      } catch (Exception e) {
         log.info("BBB {}",
               jdbcTemplate
                     .queryForObject("SELECT COUNT(*) FROM FOO WHERE BAR='BBB'", Long.class));
      }

      try {
         fooService.invokeInsertThenRollback();
      } catch (Exception e) {
         log.info("BBB {}",
               jdbcTemplate
                     .queryForObject("SELECT COUNT(*) FROM FOO WHERE BAR='BBB'", Long.class));
      }
   }

执行insertThenRollback()方法时,事务可以正常实现(运行结果:手动抛异常让事务回滚,数据‘BBB’没有插入数据库,查询条数=0), 但是执行invokeInsertThenRollback()方法时,查询结果为1,证明数据‘BBB’成功写入数据库——事务没有正常回滚
由于这两次调用唯一的区别就是,
第一次调用:外部直接调用方法insertThenRollback,
第二次调用:外部调用方法invokeInsertThenRollback(),再由方invokeInsertThenRollback()调用insertThenRollback()
由此提出问题:
调用同类方法事务不生效!
思考
加了事务注解的方法,在Bean创建时,都会通过AOP创建一个代理实例,在实际调用过程中,调用的时这个代理实例,而不是真正的FooService对象,但上述现象显然事务没有生效,是否就证明,第二次调用是直接调用的真正的FooService对象,没有走代理实例呢?

探索1

通过http://localhost:8080/actuator/beans查看Spring Bean的情况
学个Spring吧[7]-调用同类方法事务不生效问题探索_第1张图片
思考
发现fooServiceImpl只有一个代理实例,并且是单列模式,也就说不可能存在两个fooServiceImpl的bean实例,那么真正的实例对象存在哪呢?

探索2

通过Debug日志发现原始的实例对象其实是存放在代理对象的target属性里
在这里插入图片描述
思考:
什么情况会调用到target(原始实例)呢?

探索3

继续执行Debug,可以看到在执行fooService.invokeInsertThenRollback()这行代码时,会调用JdkDynamicAopProxy.invoke方法,让我们进到这个方法里看下源码(这里只粘贴了部分核心代码):

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
	……(省略)
   TargetSource targetSource = this.advised.targetSource;
   Object target = null;

   try {
      ……(省略)
      // 获取到原始实例
      target = targetSource.getTarget();
      Class<?> targetClass = (target != null ? target.getClass() : null);

      // 获取当前方法的通知拦截器链
      List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

      // 检查通知拦截器链是否为空
      if (chain.isEmpty()) {
         // 如果为空:直接调用target(原始实例)的方法,不生成方法通知
         Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
         retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
      }
      else {
         // 如果不为空,纠需要生成方法通知执行拦截器里的内容
         MethodInvocation invocation =
               new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
         //通过拦截器链进入连接点
         retVal = invocation.proceed();
      }
      ……(后续返回值的处理)
      return retVal;
   }
   finally {
      ……(省略)
   }
}

对invokeInsertThenRollback()方法进行Debug验证:
学个Spring吧[7]-调用同类方法事务不生效问题探索_第2张图片
chain返回结果为0,由此得出结论:

  1. invokeInsertThenRollback()因为没有通知拦截器链,所以直接调用target方法
  2. invokeInsertThenRollback()方法里直接通过this调用insertThenRollback()方法,这里的this也就是target,所以insertThenRollback()方法并没有走JdkDynamicAopProxy.invoke(),而是直接调用target方法,AOP切面通知也就无法生效了
    这样,我们很好地解决了 [调用同类方法事务不生效!] 问题
    感兴趣的同学可以再Debug下单独调用insertThenRollback()方法的过程,这里chain的个数为1, 返回了事务的通知拦截器,走的就是else分支
    学个Spring吧[7]-调用同类方法事务不生效问题探索_第3张图片
    进一步思考:
    Chain是在什么时候生成的呢?
探索4

进入获取chain的源码,来到DefaultAdvisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice()方法,在这里会遍历一个Advisor数组,将method(调用的方法)标注的通知匹配上对应拦截器添加到chain中返回
这里的Advisor数组在AOP代理对象创建时,会根据每个方法的注解完成初始化操作,具体源码参见: ProxyFactoryBean. initializeAdvisorChain()方法

如何解决

通过分析,我们知道了调用同类方法事务不生效问题的原因,那么在实际开发中如果遇到这类问题该如何解决呢,这里给出简单的几个思路:

1⃣️ 在实现类里面引用当前类对象

@Component
public class FooServiceImpl implements FooService {

    @Autowired
    private JdbcTemplate jdbcTemplate;
	
	// 在这里引入当前类的实体对象
    @Autowired
    private FooService fooService;

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Transactional
    public void insert() {
        jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('AAA')");
    }
    
    @Transactional(rollbackFor = RollBackException.class)
    public void insertThenRollback() throws RollBackException {
        jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('BBB')");
        throw new RollBackException();
    }

	// 通过对象实例去调用方法,这样就是通过代理实例调用,就可以走到事务切面了
    public void invokeInsertThenRollback() throws RollBackException {
        fooService.insertThenRollback();
    }
}

2⃣️给invokeInsertThenRollback()方法加上事务
编程式:

    public void invokeInsertThenRollback() throws RollBackException {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
           @SneakyThrows
           @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
               insertThenRollback();
           }
        });
    }

申明式:

    @Transactional(rollbackFor = RollBackException.class)
    public void invokeInsertThenRollback() throws RollBackException {
        insertThenRollback();
    }

至此, 我们通过一层层探索,顺利解决了问题~~~
每多看懂一行源码 就又进步了一点哟

你可能感兴趣的:(学个Spring吧)