关于springboot:避坑Around与Transactional混用导致事务不回滚

1次阅读

共计 8401 个字符,预计需要花费 22 分钟才能阅读完成。

前言

上个月,共事出于好奇在群里问 AOP 的盘绕告诉与事务注解混合用会不会导致出现异常不回滚的状况。这个问题我一下子答复不上来,因为平时没这样用过,在好奇心的驱使下,我调试了半天终于失去后果,明天我就开展讲讲。(源码解读在最初面,感兴趣的能够看看。)

论断

首先通知大家的是,同时应用 AOP 盘绕告诉和事务注解之后,最终生成的拦截器链的绝对程序是事务的拦截器在后面,AOP 盘绕告诉的拦截器在前面。 在事务的实现中将拦截器的执行过程包裹在了 try-catch 块中,产生异样后依据配置来决定是否回滚事务。(详见 org.springframework.transaction.interceptor.TransactionInterceptor#invoke),因而事务前面的拦截器都会影响事务的执行后果。 如果在 AOP 盘绕告诉外面将拦截器链执行后果中的异样给吞掉,那么事务就会失常提交而不会回滚。

示例

业务代码

业务代码中间接抛出异样,代码如下所示。

package com.example.demo.aspect;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @Author Paul
 * @Date 2022/7/3 15:52
 */
@Service
public class CustomService {@Transactional(rollbackFor = Exception.class)
  public void echo(){
    boolean s = true;
    if (s){throw new RuntimeException("test");
    }
    System.out.println("Hello------");
  }

}

盘绕告诉

盘绕告诉中捕获异样并打印日志,代码如下所示。

package com.example.demo.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * @Author Paul
 * @Date 2022/7/3 15:49
 */
@Component
@Aspect
public class CustomAspect {@Pointcut("execution(* com.example.demo.aspect..*(..))")
    public void pointcut(){}

    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint){
        try {joinPoint.proceed();
        } catch (Throwable throwable) {throwable.printStackTrace();
        }
    }

}

测试类

这里是用 get 申请来测试(原本应该用 unit test 来测试的,然而懒得写代码了,手动测试和 UT 的成果一样),代码如下所示。

package com.example.demo.controller;

import com.example.demo.aspect.CustomService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

@RestController
@RequestMapping("/home")
public class HomeController {

    private CustomService customService;

    @Autowired
    public void setCustomService(CustomService customService) {this.customService = customService;}

    @GetMapping("/echo")
    public String echo(){customService.echo();
        return new Date().toString();
    }
}

打断点

间接 debug 更加清晰,给出几个要害的地位,不便大家定位(记得下载 spring 源码再 debug,不然会跟我给出的行数对不上)。

  • org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction line:388 和 407(别离对应事务往后执行和最初提交事务的代码)
  • com.example.demo.aspect.CustomAspect#around line:24 和 26(自定义盘绕告诉的代码中的 joinPoint.proceed(),以及异样解决的中央)
  • com.example.demo.aspect.CustomService#echo line:18(业务代码中抛出异样的中央)

成果形容

启动我的项目后间接用 GET 申请拜访本地http://localhost:8080/home/echo,你会发现断点的执行程序是 org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction line:388com.example.demo.aspect.CustomAspect#around line:24com.example.demo.aspect.CustomService#echo line:18com.example.demo.aspect.CustomAspect#around line:26org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction line:407

景象:同时有盘绕告诉和事务时,在业务代码中抛出的异样会先被盘绕告诉解决,所以前面事务会失常提交而不会回滚。

现实情况

事实中,咱们很多人喜爱用盘绕告诉解决一些通用逻辑,那为什么没有呈现 bug 呢?这里我列举下,盘绕告诉的常见应用场景如下。

  • 打日志
  • 流控

这些场景的独特特点是它们都是在入口层(也就是 Web 层)做的盘绕告诉,而咱们通常不会把所有逻辑都写在入口层(如果你真的这么做了,我置信 Code Review 不会通过),而是有一个逻辑解决层(也就是 service 层)对入口层的数据进行专门解决。因而,咱们的事务注解也大都标注在逻辑解决层的办法上。这样看来,的确很少有下面提到的场景。(这里咱们能够看到,恪守开发标准在肯定水平上对开发效率是有晋升的,它能够躲避一些 bug)

源码解读

阐明: 在下面咱们介绍了拦截器链的程序是事务在前,盘绕告诉在后。这里解读源码的目标是为了搞清楚为什么事务的拦截器在前,盘绕告诉的拦截器在后。

咱们晓得事务和盘绕告诉的最终实现都是通过 AOP,而 spring 默认的 AOP 结构类就是 org.springframework.aop.framework.CglibAopProxy,通过 getProxy() 实现结构,而getCallbacks() 就是结构的要害,能够看到要害代码在 DynamicAdvisedInterceptor 中,在这个类中实现了拦截器链的结构。(代码就不展现 AOP 代码了,这里波及到 AOP 的太多常识,有趣味的能够分割我,我能够独自讲讲)。


/**
 * General purpose AOP callback. Used when the target is dynamic or when the
 * proxy is not frozen.
 */
private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {

  private final AdvisedSupport advised;

  public DynamicAdvisedInterceptor(AdvisedSupport advised) {this.advised = advised;}

  @Override
  @Nullable
  public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    Object oldProxy = null;
    boolean setProxyContext = false;
    Object target = null;
    TargetSource targetSource = this.advised.getTargetSource();
    try {if (this.advised.exposeProxy) {
        // Make invocation available if necessary.
        oldProxy = AopContext.setCurrentProxy(proxy);
        setProxyContext = true;
      }
      // Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
      target = targetSource.getTarget();
      Class<?> targetClass = (target != null ? target.getClass() : null);
      List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
      Object retVal;
      // Check whether we only have one InvokerInterceptor: that is,
      // no real advice, but just reflective invocation of the target.
      if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
        // We can skip creating a MethodInvocation: just invoke the target directly.
        // Note that the final invoker must be an InvokerInterceptor, so we know
        // it does nothing but a reflective operation on the target, and no hot
        // swapping or fancy proxying.
        Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
        retVal = methodProxy.invoke(target, argsToUse);
      }
      else {
        // We need to create a method invocation...
        retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();}
      retVal = processReturnType(proxy, target, method, retVal);
      return retVal;
    }
    finally {if (target != null && !targetSource.isStatic()) {targetSource.releaseTarget(target);
      }
      if (setProxyContext) {
        // Restore old proxy.
        AopContext.setCurrentProxy(oldProxy);
      }
    }
  }

  @Override
  public boolean equals(@Nullable Object other) {
    return (this == other ||
      (other instanceof DynamicAdvisedInterceptor &&
        this.advised.equals(((DynamicAdvisedInterceptor) other).advised)));
  }

  /**
   * CGLIB uses this to drive proxy creation.
   */
  @Override
  public int hashCode() {return this.advised.hashCode();
  }
}

下面能够看到拦截器链是通过 this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); 来结构的,最终其实走到org.springframework.aop.framework.DefaultAdvisorChainFactory#getInterceptorsAndDynamicInterceptionAdvice,简化后的代码如下。

public class DefaultAdvisorChainFactory implements AdvisorChainFactory, Serializable {

    @Override
    public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Advised config, Method method, @Nullable Class<?> targetClass) {

        // This is somewhat tricky... We have to process introductions first,
        // but we need to preserve order in the ultimate list.
        AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
        Advisor[] advisors = config.getAdvisors();
        List<Object> interceptorList = new ArrayList<>(advisors.length);
        Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
        Boolean hasIntroductions = null;

        for (Advisor advisor : advisors) {if (advisor instanceof PointcutAdvisor) {
                // Add it conditionally.
                PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
                if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
                    boolean match;
                    if (mm instanceof IntroductionAwareMethodMatcher) {if (hasIntroductions == null) {hasIntroductions = hasMatchingIntroductions(advisors, actualClass);
                        }
                        match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions);
                    }
                    else {match = mm.matches(method, actualClass);
                    }
                    if (match) {MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
                        if (mm.isRuntime()) {// Creating a new object instance in the getInterceptors() method
                            // isn't a problem as we normally cache created chains.
                            for (MethodInterceptor interceptor : interceptors) {interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
                            }
                        }
                        else {interceptorList.addAll(Arrays.asList(interceptors));
                        }
                    }
                }
            }
            else {Interceptor[] interceptors = registry.getInterceptors(advisor);
                interceptorList.addAll(Arrays.asList(interceptors));
            }
        }

        return interceptorList;
    }

}

能够看到,最终还是通过 Ioc 中的 org.springframework.aop.Advisor 来失去最终的拦截器链。代码外面是遍历 Advisor,判断是否符合条件,把符合条件的拦截器放入最终后果。因而 Advisor 的绝对程序和拦截器链的绝对程序是统一的。

而在 SpringBoot 启动的时候,会通过 spring.factories 中配置的绝对程序来主动拆卸模块。Aop 先于事务装载,在装载 Aspectj 相干模块时会将 org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator 注册到 IOC 容器中。当拆卸事务时,也向 IOC 容器中注入了 org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor。在通过 Aop 生成的 Advisor 时,会通过org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findCandidateAdvisors 来找所有的 Advisor,而此时还在 IOC 刷新阶段,只有事务注册了 Advisor,因而会先加载事务相干的 Advisor。(具体代码在 org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors 有趣味的能够自行查阅)。而到 DI 时再去生成拦截器链时,就会发现事务的拦截器永远在最后面

举荐读物

《Spring 技术底细》— 计文柯

这本书我重复读了 3 遍以上。尽管书是 12 年出版的,基于 Spring Framework 4.X 进行解说,版本有些旧。然而,当你读完这本书再去看 Spring Framework 5.x 你会发现书上讲的 spring 核心思想在最新版本中并没有产生太多变动,只是有了些加强。在咱们对 Spring 外围还不太理解的时候如果间接上手最新版本可能会有些简单,因为有很多优化实现,这样容易让咱们陷入细节太深不太能看到零碎的全貌。

正文完
 0