关于 Spring 事务注解 this 调用失效的一个细节

本文详细解析了Spring事务注解在this调用场景下为何失效,通过源码剖析和MyBatis动态代理实例,揭示了事务生效的关键在于目标对象而非代理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

下面这个是一个非常经典的 Spring 事务注解失效的例子:

@Serevice
public class TestService {
    @Resource
    private TestMapper testMapper;

    @Transactional
    public void method1() {
        int re = testMapper.insert();
        int a = 1/0;
    }

    public void method2(){
        this.method1(); //method1 事务失效
    }

}

我们分析原因的时候都会提一句:由于是 this 调用造成事务注解失效。这个说法本身没有问题,但还是没有描述清楚一个细节。

我们先想一下这个调用过程,比如是一个 TestController 来调用这个 TestService,那么本质其实是 TestController 调用的 TestService 的代理类 TestService$Proxy,那么代理类其中的 method1() 方法就是一个代理方法。那么 TestController 调用 TestService#method2 -> this#method1 的时候,这个 this 不是代理类 TestService$Proxy 的实例嘛,前面已经说了,代理类 TestService$Proxymethod1() 方法就是一个代理方法,那此时 this(即代理类 TestService$Proxy 实例)调用的 method1() 就是一个代理方法。既然是代理方法,理应事务要生效的。

关于这个问题我们可以从两个角度分析。首先从源码角度进行分析,抓大放小,既然是分析 AOP 相关的,那么肯定是看 org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor#intercept

		@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())) {
          //如果拦截器链是空的 ,且是 public 方法
					// 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...
          //要使用 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);
				}
			}
		}

继续看 org.springframework.aop.framework.ReflectiveMethodInvocation#proceed 方法:

	@Override
	@Nullable
	public Object proceed() throws Throwable {
		//	We start with an index of -1 and increment early.
		if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
			return invokeJoinpoint();
		}

		Object interceptorOrInterceptionAdvice =
				this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
		if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
			// Evaluate dynamic method matcher here: static part will already have
			// been evaluated and found to match.
			InterceptorAndDynamicMethodMatcher dm =
					(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
			Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
			if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
				return dm.interceptor.invoke(this);
			}
			else {
				// Dynamic matching failed.
				// Skip this interceptor and invoke the next in the chain.
				return proceed();
			}
		}
		else {
			// It's an interceptor, so we just invoke it: The pointcut will have
			// been evaluated statically before this object was constructed.
			return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
		}
	}

要注意这是一个拦截器链,当最后一个拦截器执行完成后就会执行 org.springframework.aop.framework.ReflectiveMethodInvocation#invokeJoinpoint -> org.springframework.aop.support.AopUtils#invokeJoinpointUsingReflection 方法:

	// target是目标对象
  @Nullable
	public static Object invokeJoinpointUsingReflection(@Nullable Object target, Method method, Object[] args)
			throws Throwable {

		// Use reflection to invoke the method.
		try {
			ReflectionUtils.makeAccessible(method);
      //执行的目标对象的方法
			return method.invoke(target, args);
		}
		catch (InvocationTargetException ex) {
			// Invoked method threw a checked exception.
			// We must rethrow it. The client won't see the interceptor.
			throw ex.getTargetException();
		}
		catch (IllegalArgumentException ex) {
			throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" +
					method + "] on target [" + target + "]", ex);
		}
		catch (IllegalAccessException ex) {
			throw new AopInvocationException("Could not access method [" + method + "]", ex);
		}
	}

到这里其实就已经水落石出了。虽然 Spring 搞了很多拦截器去增强目标方法,但实际执行被代理方法的时候还是靠目标对象去执行的(这也可以说明代理类是会持有被代理类的引用的),所以此时的 this 并不是代理对象,而是被代理对象,所以事务注解也就失效了。

上面主要是通过源码分析的角度来说明为啥 this 调用会造成事务注解失效。因为目标方法实际就是被代理对象执行的,所以这个 this 就是被代理对象。但是代理对象毕竟还是显得有点看不见摸不着,接下来结合《再议 MyBatis 中的动态代理》,看下生成的代理对象(JDK 的动态代理,虽然与 CGLib 有区别,但是在本文的议题中是不影响的)长啥样:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import com.example.myBatisDemo.proxy.ITarget;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements ITarget {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final void show() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

...
}

可以看到生成的代理类 $Proxy0 实现了 ITarget 接口,实际执行 show() 方法的时候,会执行 invoke() 方法,也就是 JdkProxyBuilder (细节见《再议 MyBatis 中的动态代理》)的 invoke() 方法,然后 JdkProxyBuilder 又持有目标对象的引用,通过反射执行了目标对象的方法。

也就是说本质上这个目标方法还是被目标对象执行的,代理类只是包了一层。所以用 this 调用去解释文章开头 Spring 事务注解失效的场景是没毛病的,只是这样描述会容易引起歧义。

References

欢迎关注公众号:
在这里插入图片描述

### Spring同一种类内部方法调用事务失效的原因分析 在Spring框架中,声明式事务是基于AOP(面向切面编程)实现的。具体来说,当在一个方法上添加`@Transactional`注解时,Spring会通过动态代理的方式对该方法进行增强,从而实现了事务管理功能。然而,在同一类内的方法调用场景下,由于Java语言本身的特性以及Spring AOP的工作原理,可能导致事务失效。 #### 原因解析 在同一类内,假设存在两个方法一个是带有`@Transactional`注解方法(称为事务方法),另一个是没有事务注解方法(称为非事务方法)。如果非事务方法直接调用事务方法,则该调用实际上是对当前对象自身的调用(即`this.method()`的形式)。这种情况下,调用的目标仍然是原始的对象而非经过Spring代理后的对象[^1]。因此,事务拦截器无法介入此次调用过程,导致事务失效。 --- ### 解决方案详解 以下是几种常见的解决方案及其适用场景: #### 方法一:将事务方法抽取至独立的服务类 可以通过重构代码结构,将原本位于同一个服务类中的事务方法移动到一个新的服务类中。这样做的好处在于强制使事务方法通过外部接口被调用,从而确保每次调用都经过Spring代理层,进而激活事务支持[^2]。 示例代码如下: ```java @Service public class MyService { @Autowired private AnotherService anotherService; public void nonTransactionMethod() { // 调用AnotherService中的事务方法 anotherService.transactionalMethod(); } } @Service public class AnotherService { @Transactional public void transactionalMethod() { // 此处为事务逻辑 } } ``` --- #### 方法二:利用`ApplicationContext`获取代理对象并调用目标方法 另一种方式是在需要调用事务方法的地方,借助Spring容器提供的上下文工具类`ApplicationContext`来获取当前Bean的代理实例,并通过此代理实例完成对事务方法调用。这种方法虽然有效,但在实际开发中可能显得不够优雅[^3]。 示例代码如下: ```java @Autowired private ApplicationContext applicationContext; public void nonTransactionMethod() { MyService proxyInstance = (MyService) applicationContext.getBean(MyService.class); proxyInstance.transactionalMethod(); // 使用代理对象调用事务方法 } @Transactional public void transactionalMethod() { // 此处为事务逻辑 } ``` --- #### 方法三:启用代理对象暴露机制并通过`AopContext.currentProxy()`访问代理对象 为了简化上述第二种方法的操作流程,可以在应用启动配置阶段开启代理对象暴露选项(即设置`@EnableAspectJAutoProxy(exposeProxy=true)`属性)。之后即可通过静态方法`AopContext.currentProxy()`快速获得当前线程绑定的代理对象[^5]。 示例代码如下: ```java @EnableAspectJAutoProxy(exposeProxy = true) @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } @Service public class MyService { @Autowired private EmployeeRepository employeeRepository; public void nonTransactionMethod() { ((MyService) AopContext.currentProxy()).transactionalMethod(); } @Transactional public void transactionalMethod() { // 执行数据库操作... } } ``` 需要注意的是,使用这种方式的前提条件之一就是必须显式指定允许暴露代理对象;否则运行期间将会抛出异常提示未找到有效的代理实例。 --- #### 方法四:调整设计模式避免同类间直接调用 最后还可以考虑从业务层面重新审视现有架构是否存在过度耦合的情况。比如尝试引入事件驱动模型或者命令查询职责分离(CQRS)原则等方式减少不必要的跨层次交互频率,从根本上规避掉此类问题的发生可能性[^4]。 --- ### 总结 综上所述,对于Spring框架下的相同类别成员函数之间相互调用引发的事务丢失现象,主要源于其底层依赖于CGLIB字节码生成技术构建而成的代理机制所致。针对这一情况提供了多种可行的技术手段予以应对,开发者可以根据项目的具体情况灵活选用最合适的策略加以实施。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值