Spring - 事务(2)- 同一类中方法调用事务不起作用

参考:

  • https://segmentfault.com/a/1190000008379179
  • https://blog.csdn.net/aya19880214/article/details/50640596

一、概述

用过Spring声明式事务的小伙伴肯定都知道,有这种一种场景:

  • 一个类中有两个方法testA()、testB(),testA()没有使用事务,testB()使用事务(默认传播机制为REQUIRED:支持当前事务,如果不存在则创建一个新事务)
    • 场景1:通过 service#testB() 方式调用:那么testB()是有事务的,默认传播机制的原因
    • 场景2:通过 service#testA() -> testA()内部 this.testB() 调用:那么testB()是没有事务的

上面2个场景的结果大家都是知道的,但是仔细想了下就有点蒙了。

Spring的声明式事务是通过Spring AOP代理实现的,默认情况下接口通过JDK代理实现、普通类通过CGLIB代理实现。

通常,我们理解的CGLIB代理,相当于就是通过一个子类在进行增强,那么testA()内部调用testB(),因为java多态的原因,也是调用子类(代理类)来完成,那么也是有增强的逻辑在的,那么为什么Sring中的CGLIB代理就不行,,也就是testB()的事务为什么不起作用?(我之前一直以为Spring AOP中的CGLIB代理和普通的CGLIB代理实现逻辑是一样的)

下面就带着这个问题来捋一捋Spring AOP中的CGLIB代理和我们认为的CGLIB有什么不同。

二、CGLIB代理

2.1 CGLIB代理示例

回顾下CGLIB代理生成代理类并调用目标方法流程。

执行下面的代码后,可以发现testA()内部调用testB()时,也触发了增强逻辑,也就是调用了代理类。控制台打印结果如下:

====== 执行方法开始:testA ======
====== 执行方法开始:testB ======
====== 执行方法结束:testB ======
====== 执行方法结束:testA ======

示例代码如下:

// 测试类
public class TestService {
    public void testA() {
        this.testB();
    }
    public void testB() {
    }
}

// 生成CGLIB代理,并通过代理调用目标方法
public static void main(String[] args) {
    // 代理类class文件存入本地磁盘,后面可以通过反编译查看源码
    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "doc");
    
    // 通过CGLIB动态代理获取代理对象的过程
    Enhancer enhancer = new Enhancer();
    // 设置enhancer对象的父类
    enhancer.setSuperclass(TestService.class);
    // 设置enhancer的回调对象
    enhancer.setCallback(new MethodInterceptor() {
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            System.out.println(String.format("====== 执行方法开始:%s ======", method.getName()));
            Object object = methodProxy.invokeSuper(o, objects);
            System.out.println(String.format("====== 执行方法结束:%s ======", method.getName()));
            return object;
        }
    });
    // 创建代理对象
    TestService testService = (TestService) enhancer.create();
    // 通过代理对象调用目标方法
    testService.testA();
}

2.2 CGLIB生成的代理类

2.1中通过System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "doc");保存了生成的代理类class文件,通过反编译看下代理类。

代理类继承了TestService,同时重写了testA()、testB()方法。

  • 代理类调用testA()时,就会进入我们定义的匿名内部类 - 方法拦截器中:MethodInterceptor#intercept()
  • intercept()方法中执行增强逻辑,并调用TestService#testA()
  • testA()方法中调用了testB(),因为继承的关系,又会调用代理类的testB(),之后的逻辑和testA()一样

CGLIB代理类反编译后源码部分:

public class TestService$$EnhancerByCGLIB$$3e52c528 extends TestService implements Factory {
	public final void testA() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

		final void CGLIB$testA$1() {
        	super.testA();
    	}

        if (var10000 != null) {
            var10000.intercept(this, CGLIB$testA$0$Method, CGLIB$emptyArgs, CGLIB$testA$0$Proxy);
        } else {
            super.testA();
        }
    }
    
    public final void testB() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            var10000.intercept(this, CGLIB$testB$1$Method, CGLIB$emptyArgs, CGLIB$testB$1$Proxy);
        } else {
            super.testB();
        }
    }

	final void CGLIB$testB$0() {
        super.testB();
    }

	// 其他代码省略
}

一开始我的疑问也在这里,自己通过CGLIB代理模拟了代码增强的逻辑,同一类中内部方法调用也触发了代理类增强,怎么Spring AOP中的CGLIB代理怎么不触发呢。

三、Spring AOP

带着上面的疑问,通过debug观察一下代理类调用目标方法的流程。

3.1 示例1:testA()没有事务、testB()声明使用事务

(1)场景1:Controller#test()接口中只调用testA()方法

发现Controller中注入的不是service代理类实例,是service本身的实例,debug直接进入testA()方法,testA()调用testB()也是一样,不会触发代理逻辑

(2)场景2:Controller#test()接口中先调用testA()、再调用testB()

此时发现注入的service是CGLIB代理类的实例了,debug时进入org.springframework.aop.framework.CglibAopProxy#intercept()中。

我们重点观察下这个方法到底干了啥,是否和我们2.2节中生成的CGLIB代理类逻辑一样:该方法主要流程:

  • 获取目标类
  • 查找目标方法、类的拦截器和动态代理拦截器
    • 若无拦截器且是public方法:通过反射调用目标类的目标方法

通过查看源码,发现Spring AOP不是通过代理类来调用testA(),而是通过目标类进行反射调用,这样就可以解释了为什么testB()没有事务,因为不是直接通过代理类来完成调用的,也就无法触发代理逻辑了。


示例代码:

public class TransactionController {

    @Autowired
    private TransactionService transactionService;

    @GetMapping("/test")
    public void test() {
    	// 场景1,只调用testA()
        transactionService.testA();
        // 场景2,调用testA()、testB()
        // transactionService.testB();
    }
}

@Service
public class TransactionService {
    @Autowired
    private TransactionDao transactionDao;

    public void testA() {
        transactionDao.insert(bean);
        testA();
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void testB() {
        transactionDao.insert(bean);
        int i = 1 / 0;
    }
}

注:
Spring生成的代理类设置保存到磁盘:

  • Spring AOP - CGLIB:System.setProperty(org.springframework.cglib.core.DebuggingClassWriter.DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, “proxy/spring/cglib”);

CglibAopProxy#intercept()源码如下:

// CglibAopProxy#intercept()
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
	Object oldProxy = null;
	boolean setProxyContext = false;
	Class<?> targetClass = null;
	Object target = null;
	try {
		if (this.advised.exposeProxy) {
			// Make invocation available if necessary.
			oldProxy = AopContext.setCurrentProxy(proxy);
			setProxyContext = true;
		}
		
		// 获取目标类,可能为null,延迟获取
		// 例如示例中的TransactionService实例,非代理类实例
		target = getTarget();
		if (target != null) {
			targetClass = target.getClass();
		}
		// 获取拦截器和动态代理拦截器,例如:事务拦截器
		List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
		Object retVal;
		// 没有拦截器,直接通过反射调用目标类的目标方法
		// 源码里注释也提到,这种情况可以直接调用目标类的目标方法,不需要创建MethodInvocation
		if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
			// 假如必要的话适配方法的参数
			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) {
			releaseTarget(target);
		}
		if (setProxyContext) {
			// Restore old proxy.
			AopContext.setCurrentProxy(oldProxy);
		}
	}
}

3.2 示例2:testA()、testB()都声明使用事务(REQUIRED、REQUIRES_NEW)

(1)场景1:Controller#test()接口中只调用testA()方法

  • 预期:testA()启用一个事务,testB()新建一个事务,testA()中抛出异常,不影响testB()所在的事务,所以b可以正常保存,a不可以保存
  • 结果:a、b都无法保存

示例代码:

public class TransactionService {
    @Autowired
    private TransactionDao transactionDao;

	@Transactional(propagation = Propagation.REQUIRED)
    public void testA() {
        transactionDao.insert("a");
        testB();
        int i = 1 / 0;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void testB() {
        transactionDao.insert("b");
    }
}

由上面示例中可以猜测下,testB()的事务失效了,所以testA()和testB()在同一个事务中,testB()报错造成整个事务回滚了。

debug时仍然是进入org.springframework.aop.framework.CglibAopProxy#intercept()中,该方法主要流程示例1中也简单分析过,这里主要是找到了拦截器:

  • 获取目标类
  • 查找目标方法、类的拦截器和动态代理拦截器(例如事务就是:TransactionInterceptor
    • 若有拦截器,创建一个CglibMethodInvocation,并调用父类ReflectiveMethodInvocationproceed()进行处理
      • proceed()方法中,第一次会进入TransactionInterceptor#invoke()进行事务相关逻辑的处理
      • 当事务准备好后就会触发执行目标方法,又回到了ReflectiveMethodInvocation#proceed()方法中,第二次会执行CglibAopProxy#invokeJoinpoint()方法,完成调用目标方法的操作
      • 重点就在这个invokeJoinpoint()方法中,执行的是目标类的目标方法,而不是我之前所认为的代理类,所以就和3.1节 示例1的情况对应起来了,内部调用时事务确实是不起作用的。

到这里就比较清晰了,最终都是通过 代理类 完成逻辑增强,目标类 完成方法调用的,而不是代理类直接完成的,所以testA()调用testB()是不会触发代理逻辑的,相当于一个普通方法,和testA()在同一个事务中。

相关Spring源码如下:

// ReflectiveMethodInvocation#proceed()
public Object proceed() throws Throwable {
	// currentInterceptorIndex第一次是-1
	// 当完成事务准备工作后,就会进入这个分支
	if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
		// 执行连接点,即目标方法
		return invokeJoinpoint();
	}

	// currentInterceptorIndex+1
	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;
		if (dm.methodMatcher.matches(this.method, this.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 {
		// 执行拦截器的invoke()方法
		// 例如事务拦截器:TransactionInterceptor#invoke()
		// testA()的事务就是从这里进入的
		return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
	}
}

// CglibAopProxy#invokeJoinpoint()
protected Object invokeJoinpoint() throws Throwable {
	// public方法
	if (this.publicMethod) {
		// 调用目标类的目标方法,注意这里的this.target,是真正的目标类,不是代理类
		return this.methodProxy.invoke(this.target, this.arguments);
	} else {
		// 非public,设置method.setAccessible(true);,再进行反射调用
		return super.invokeJoinpoint();
	}
}

再回到之前我们写的的CGLIB代理Demo中,初始时我们执行 代理类实例.testA(),进入方法拦截器中:MethodInterceptor#intercept(),方法拦截器中执行完增强逻辑后需要执行目标类的testA(),关键点就在这里,我们执行时调用的是 Object object = methodProxy.invokeSuper(o, objects);:这里的o是代理类的实例,类似的就是传递的是目标类的实例,所以最终执行目标类的super.testA(),testA()调用了testB(),仍然是代理类的testB(),所以也会有增强逻辑,这可以解释Demo中增强逻辑为什么生效了。

Demo中invokeSuper()相关源码:

public class MethodProxy {
	
	public Object invokeSuper(Object obj, Object[] args) throws Throwable {
        try {
            this.init();
            // obj就是传递过来的o,代理类实例, 类似的地方就是spring中这里传递的是目标类实例
            MethodProxy.FastClassInfo fci = this.fastClassInfo;
            // 执行目标方法,需要注意这里执行的是 代理类中的CGLIB$testA$1(),内部执行了super.testA()
            return fci.f2.invoke(fci.i2, obj, args);
        } catch (InvocationTargetException var4) {
            throw var4.getTargetException();
        }
    }
}

public class TestService$$EnhancerByCGLIB$$3e52c528$$FastClassByCGLIB$$97057721 extends FastClass {
	
	public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
		// 这里就是上文的代理类实例
		3e52c528 var10000 = (3e52c528)var2;
		// 要执行方法的索引,例如要执行testA()的目标类方法,那么就是7
        int var10001 = var1;
        try {
            switch(var10001) {
				case 6:
                	var10000.CGLIB$testB$0();
                	return null;
            	case 7:
            		// 可以查看上面的代理类CGLIB$testA$1()方法,内部就是 super.testA()
                	var10000.CGLIB$testA$1();
                	return null;
                // 省略	
			}
	}
}

四、解决方法

常见的解决方法:

  • 通过代理类调用
    • 注入自身
    • 拆成两个类
    • AopContext.currentProxy()获取代理类
  • 使用编程式事务

4.1 注入自身/拆成两个类

既然内部调用不起作用,那就使用代理对象来调用:

  • 方式一:通过在AService中注入自己 selfService,再用selfService#testB()
  • 方式二:拆成两个类,如AService、BService,testA()中通过bService#testB()

方式一的示例代码如下:

public class TransactionService {
    @Autowired
    private TransactionDao transactionDao;

	@Autowired
    private TransactionService transactionService;

	@Transactional(propagation = Propagation.REQUIRED)
    public void testA() {
        transactionDao.insert("a");
        // 注入的代理类来调用
        transactionService.testB();
        int i = 1 / 0;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void testB() {
        transactionDao.insert("b");
    }
}

4.2 @EnableAspectJAutoProxy

  • SpringBoot上启动类上添加@EnableAspectJAutoProxy(exposeProxy = true)注解,同时设置exposeProxy=true
  • testA()中通过 (TransactionService) AopContext.currentProxy()获取代理类
  • 通过代理类调用testB()

示例代码:

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

public class TransactionService {
    @Autowired
    private TransactionDao transactionDao;

	@Transactional(propagation = Propagation.REQUIRED)
    public void testA() {
        transactionDao.insert("a");
        // 获取代理类来调用testB()
        TransactionService transactionService = (TransactionService) AopContext.currentProxy();
        transactionService.testB();
        
        int i = 1 / 0;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void testB() {
        transactionDao.insert("b");
    }
}

五、总结

  • 同一类中调用事务不起作用
  • 若需要同一类中也起作用,那么需要通过代理类完成调用,才能触发增强逻辑
  • Spring AOP中会生成两个类:目标类、目标类的代理类,通过代理类完成增强逻辑,目标类完成实际的调用(这种统一的处理方式有好有坏,像CGLIB这种代理方式是通过子类完成的,也被当做普通的代理类)
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值