IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Spring - 事务(2)- 同一类中方法调用事务不起作用 -> 正文阅读

[Java知识库]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$$c4eb28c 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;
        }

        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();
        }
    }

	// 其他代码省略
}

一开始我的疑问也在这里,自己通过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;
    }
}

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();
	}
}

四、解决方法

常见的解决方法:

  • 通过代理类调用
    • 注入自身
    • 拆成两个类
    • 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这种代理方式是通过子类完成的,也被当做普通的代理类)
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-08-23 16:31:00  更:2021-08-23 16:32:46 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 9:58:01-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码