面试必备技能:JDK动态代理给Spring事务埋下的坑!

2024-05-24 02:32

本文主要是介绍面试必备技能:JDK动态代理给Spring事务埋下的坑!,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、场景分析

最近做项目遇到了一个很奇怪的问题,大致的业务场景是这样的:我们首先设定两个事务,事务parent和事务child,在Controller里边同时调用这两个方法,示例代码如下:

1、场景A:

@RestController
@RequestMapping(value = "/test")
public class OrderController {@Autowiredprivate TestService userService;@GetMappingpublic void test() {//同时调用parent和childuserService.parent();userService.child();}
}
@Service
public class TestServiceImpl implements TestService {@Autowiredprivate UserMapper userMapper;@Override@Transactionalpublic void parent() {User parent = new User("张大壮 Parent", "123456", 45);userMapper.insert(parent);}@Override@Transactionalpublic void child() {User child = new User("张大壮 Child", "654321", 25);userMapper.insert(child);}
}

这里其实是分别执行了两个事务,执行的结果是两个方法都可以插入数据!如下:

这里写图片描述

2、场景B:

修改上述代码如下:

@RestController
@RequestMapping(value = "/test")
public class OrderController {@Autowiredprivate TestService userService;@GetMappingpublic void test() {userService.parent();}
}
@Service
public class TestServiceImpl implements TestService {@Autowiredprivate UserMapper userMapper;@Override@Transactionalpublic void parent() {User parent = new User("张大壮 Parent", "123456", 45);userMapper.insert(parent);//在parent里边调用childchild();}@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void child() {User child = new User("张大壮 Child", "654321", 25);userMapper.insert(child);}
}

Propagation.REQUIRES_NEW的含义表示:如果当前存在事务,则挂起当前事务并且开启一个新事务继续执行,新事务执行完毕之后,然后在缓刑之前挂起的事务,如果当前不存在事务的话,则开启一个新事务。

执行的结果是两个方法都可以插入数据!执行结果如下:

这里写图片描述

场景A和场景B都是正常的执行,期间没有发生任何的回滚,假如child()方法中出现了异常!

3、场景C

修改child()的代码如下所示,其他代码和场景B一样:

    @Override@Transactionalpublic void parent() {User parent = new User("张大壮 Parent", "123456", 45);userMapper.insert(parent);child();}@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void child() {User child= new User("张大壮 Child", "654321", 25);userMapper.insert(child);throw new RuntimeException("child Exception....................");}

执行结果如下,会出现异常,并且数据都没有插入进去:

这里写图片描述

这里写图片描述

疑问1:场景C中child()抛出了异常,但是parent()没有抛出异常,按道理是不是应该parent()提交成功而child()回滚?

可能有的小伙伴要说了,child()抛出了异常在parent()没有进行捕获,造成了parent()也是抛出了异常了的!所以他们两个都会回滚!

4、场景D

按照上述小伙伴的疑问这个时候,如果对parent()方法修改,捕获child()中抛出的异常,其他代码和场景C一样:

    @Override@Transactionalpublic void parent() {User parent = new User("张大壮 Parent", "123456", 45);userMapper.insert(parent);try {child();} catch (Exception e) {e.printStackTrace();}}@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void child() {User child = new User("张大壮 Child", "654321", 25);userMapper.insert(child);throw new RuntimeException("child Exception....................");}

然后再次执行,结果是两个都插入了数据库:

这里写图片描述

这里写图片描述

看到这里很多小伙伴都可能会问,按照我们的逻辑来想的话child()中抛出了异常,parent()没有抛出并且捕获了child()抛出了异常!执行的结果应该是child()回滚,parent()提交成功的啊!

疑问2:场景D为什么不是child()回滚和parent()提交成功哪?

上述的场景C和场景D似乎融为了一题,要么都成功要么都失败!和我们预期的效果一点都不一样!看到这里这就是我们今天要探讨的主题《JDK动态代理给Spring事务埋下的坑!》接下来我们就分析一下Spring事务在该特定场景下不能回滚的深层次原因!

二、问题本质所在

我们知道Spring事务管理是通过JDK动态代理的方式进行实现的(另一种是使用CGLib动态代理实现的),也正是因为动态代理的特性造成了上述parent()方法调用child()方法的时候造成了child()方法中的事务失效!简单的来说,在场景D中parent()方法调用child()方法的时候,child()方法的事务是不起作用的,此时的child()方法像一个没有加事务的普通方法,其本质上就相当于下边的代码:

场景C本质:

这里写图片描述

场景D本质:

这里写图片描述

正如上述的代码,我们可以很轻松的解释疑问1和疑问2,因为动态代理的特性造成了场景C和场景D的本质如上述代码。在场景C中,child()抛出异常没有捕获,相当于parent事务中抛出了异常,造成parent()一起回滚,因为他们本质是同一个方法;在场景D中,child()抛出异常并进行了捕获,parent事务中没有抛出异常,parent()和child()同时在一个事务里边,所以他们都成功了;

看到这里,那么动态代理的这个特性到底是什么才会造成Spring事务失效那?

三、动态代理的这个特性到底是什么?

首先我们看一下一个简单的动态代理实现方式:

这里写图片描述

//接口
public interface OrderService {void test1();void test2();
}//接口实现类
public class OrderServiceImpl implements OrderService {@Overridepublic void test1() {System.out.println("--执行test1--");}@Overridepublic void test2() {System.out.println("--执行test2--");}
}
//代理类
public class OrderProxy implements InvocationHandler {private static final String METHOD_PREFIX = "test";private Object target;public OrderProxy(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//我们使用这个标志来识别是否使用代理还是使用方法本体if (method.getName().startsWith(METHOD_PREFIX)) {System.out.println("========分隔符========");}return method.invoke(target, args);}public Object getProxy() {return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),target.getClass().getInterfaces(), this);}
}
//测试方法
public class ProxyDemo {public static void main(String[] args) {OrderService orderService = new OrderServiceImpl();OrderProxy proxy = new OrderProxy(orderService);orderService = (OrderService) proxy.getProxy();orderService.test1();orderService.test2();}
}

此时我们执行以下测试方法,注意了此时是同时调用了test1()和test2()的,执行结果如下:

这里写图片描述

可以看出,在OrderServiceImpl 类中由于test1()没有调用test2(),他们方法的执行都是使用了代理的,也就是说test1和test2都是通过代理对象调用的invoke()方法,这和我们场景A和B类似。

假如我们模拟一下场景C和场景D在test1()中调用test2(),那么代码修改为如下:

这里写图片描述

这里写图片描述

执行结果如下:

这里写图片描述

这里可以很清楚的看出来test1()走的是代理,而test2()走的是普通的方法,没有经过代理!看到这里你是否已经恍然大明白了呢?

这个应该可以很好的理解为什么是这样子!这是因为在Java中test1()中调用test2()中的方法,本质上就相当于把test2()的方法体放入到test1()中,也就是内部方法,同样的不管你嵌套了多少层,只有代理对象proxy 直接调用的那一个方法才是真正的走代理的,如下:

这里写图片描述

测试方法和上边的测试方法一样,执行结果如下:

这里写图片描述

记住:只有代理对象proxy直接调用的那个方法才是真正的走代理的!

四、如何解决这个坑?

上文的分析中我们已经了解了为什么在该特定场景下使用Spring事务的时候造成事务无法回滚的问题,下边我们谈一下几种解决的方法:

1、我们可以选择逃避这个问题!我们可以不使用以上这种事务嵌套的方式来解决问题,最简单的方法就是把问题提到Service或者是更靠前的逻辑中去解决,使用service.xxxtransaction是不会出现这种问题的。

2、通过AopProxy上下文获取代理对象:

(1)SpringBoot配置方式:注解开启 exposeProxy = true,暴露代理对象 (否则AopContext.currentProxy()) 会抛出异常。

添加依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

添加注解:

这里写图片描述

修改原有代码的执行方式为:

这里写图片描述

此时的执行结果为:

这里写图片描述

可见,child方法由于异常已经回滚了,而parent可以正确的提交,这才是我们想要的结果!注意的是在parent调用child的时候是通过try/catch捕获了异常的!

(2)传统Spring XML配置文件只需要添加依赖个设置如下配置即可,使用方式一样:

<aop:aspectj-autoproxy expose-proxy="true"/>

3、通过ApplicationContext上下文进行解决:

@Service
public class TestServiceImpl implements TestService {@Autowiredprivate UserMapper userMapper;/*** Spring应用上下文*/@Autowiredprivate ApplicationContext context;private TestService proxy;@PostConstructpublic void init() {//从Spring上下文中获取AOP代理对象proxy = context.getBean(TestService.class);}@Override@Transactionalpublic void parent() {User parent = new User("张大壮 Parent", "123456", 45);userMapper.insert(parent);try {proxy.child();} catch (Exception e) {e.printStackTrace();}}@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void child() {User child = new User("张大壮 Child", "654321", 25);userMapper.insert(child);throw new RuntimeException("child Exception....................");}
}

执行结果符合我们的预期:

这里写图片描述

五、总结

到此为止,我们简单的介绍了一下Spring事务管理中如果业务中有像场景C或者场景D的情况时,如果不清楚JDK动态代理造成Spring事务无法回滚的问题的话就可能是一个开发事故了,说不定是要扣工资的!

上文中简述了几种场景的事务使用和造成事务无法回滚的根本问题,当然讲述的还是表面的现象,并没有深入原理去分析,尽管如此,如果你在面试的时候能够对这个问题说一下自己的了解,也是一个加分项!

这篇关于面试必备技能:JDK动态代理给Spring事务埋下的坑!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/997117

相关文章

Java实现在Word文档中添加文本水印和图片水印的操作指南

《Java实现在Word文档中添加文本水印和图片水印的操作指南》在当今数字时代,文档的自动化处理与安全防护变得尤为重要,无论是为了保护版权、推广品牌,还是为了在文档中加入特定的标识,为Word文档添加... 目录引言Spire.Doc for Java:高效Word文档处理的利器代码实战:使用Java为Wo

SpringBoot日志级别与日志分组详解

《SpringBoot日志级别与日志分组详解》文章介绍了日志级别(ALL至OFF)及其作用,说明SpringBoot默认日志级别为INFO,可通过application.properties调整全局或... 目录日志级别1、级别内容2、调整日志级别调整默认日志级别调整指定类的日志级别项目开发过程中,利用日志

Java中的抽象类与abstract 关键字使用详解

《Java中的抽象类与abstract关键字使用详解》:本文主要介绍Java中的抽象类与abstract关键字使用详解,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录一、抽象类的概念二、使用 abstract2.1 修饰类 => 抽象类2.2 修饰方法 => 抽象方法,没有

SpringBoot 多环境开发实战(从配置、管理与控制)

《SpringBoot多环境开发实战(从配置、管理与控制)》本文详解SpringBoot多环境配置,涵盖单文件YAML、多文件模式、MavenProfile分组及激活策略,通过优先级控制灵活切换环境... 目录一、多环境开发基础(单文件 YAML 版)(一)配置原理与优势(二)实操示例二、多环境开发多文件版

Spring 中的切面与事务结合使用完整示例

《Spring中的切面与事务结合使用完整示例》本文给大家介绍Spring中的切面与事务结合使用完整示例,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考... 目录 一、前置知识:Spring AOP 与 事务的关系 事务本质上就是一个“切面”二、核心组件三、完

Java实现远程执行Shell指令

《Java实现远程执行Shell指令》文章介绍使用JSch在SpringBoot项目中实现远程Shell操作,涵盖环境配置、依赖引入及工具类编写,详解分号和双与号执行多指令的区别... 目录软硬件环境说明编写执行Shell指令的工具类总结jsch(Java Secure Channel)是SSH2的一个纯J

JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法

《JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法》:本文主要介绍JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法,每种方法结合实例代码给大家介绍的非常... 目录引言:为什么"相等"判断如此重要?方法1:使用some()+includes()(适合小数组)方法2

SpringBoot 获取请求参数的常用注解及用法

《SpringBoot获取请求参数的常用注解及用法》SpringBoot通过@RequestParam、@PathVariable等注解支持从HTTP请求中获取参数,涵盖查询、路径、请求体、头、C... 目录SpringBoot 提供了多种注解来方便地从 HTTP 请求中获取参数以下是主要的注解及其用法:1

HTTP 与 SpringBoot 参数提交与接收协议方式

《HTTP与SpringBoot参数提交与接收协议方式》HTTP参数提交方式包括URL查询、表单、JSON/XML、路径变量、头部、Cookie、GraphQL、WebSocket和SSE,依据... 目录HTTP 协议支持多种参数提交方式,主要取决于请求方法(Method)和内容类型(Content-Ty

深度解析Java @Serial 注解及常见错误案例

《深度解析Java@Serial注解及常见错误案例》Java14引入@Serial注解,用于编译时校验序列化成员,替代传统方式解决运行时错误,适用于Serializable类的方法/字段,需注意签... 目录Java @Serial 注解深度解析1. 注解本质2. 核心作用(1) 主要用途(2) 适用位置3