前言
上个月,共事出于好奇在群里问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 */@Servicepublic 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@Aspectpublic 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:388
-- com.example.demo.aspect.CustomAspect#around line:24
-- com.example.demo.aspect.CustomService#echo line:18
-- com.example.demo.aspect.CustomAspect#around line:26
-- org.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外围还不太理解的时候如果间接上手最新版本可能会有些简单,因为有很多优化实现,这样容易让咱们陷入细节太深不太能看到零碎的全貌。