Spring 事务失效了,怎么办?
这是小伙伴们在微信上问的一个问题:
这个问题比较典型,让我想到面试时有一个 Spring 事务失效的问题,跟这个原因以及解决方案是一模一样的,因此,抽空整篇文章和小伙伴们分享下。
1. AOP 的原理
小伙伴们知道,AOP 底层就是动态代理,动态代理有两种实现方式:
- JDK 动态代理:利用拦截器(必须实现 InvocationHandler)加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用 InvokeHandler 来处理。举个例子,假设有一个接口 A,A 有一个实现类 B,现在要给 B 生成代理对象,那么实际上是给 A 接口自动生成了一个匿名实现类,并且在这个匿名实现类中调用到 B 中的方法。
- CGLIB 动态代理:利用 ASM 框架,对代理对象类生成的 class 文件加载进来,通过修改其字节码生成子类来处理。举个例子,现在有一个类 A,A 没有接口,现在想给 A 生成一个代理对象,那么实际上是自动给 A 生成了一个子类,在这个子类中覆盖了 A 中的方法,所以,小伙伴们要注意,A 类以及它里边的方法不能是 final 类型的,否则无法生成代理。
如果被代理的对象有接口,则可以使用 JDK 动态代理,没有接口就可以使用 CGLIB 动态代理。
在 Spring 中,默认情况下,如果被代理的对象有接口,就使用 JDK 动态代理,如果被代理的对象没有接口,则使用 CGLIB 动态代理。
在 Spring Boot 中,2.0 之前也跟 Spring 中的规则一样,2.0 之后则统一都使用 CGLIB 动态代理。
不过这些都是默认的规则,如果有接口,但是你又希望使用 CGLIB 动态代理,通过修改配置,也都是可以实现的:
如果是 XML 配置,想使用 CGLIB 动态代理,可以按如下方式实现:
<aop:config proxy-target-class="true"> <aop:pointcut id="pc1" expression="。。。" /> <aop:aspect ref="logAdvice"> 。。。 </aop:aspect> </aop:config>
如果是 Java 配置,想使用 CGLIB 动态代理,可以按如下方式实现:
@Component @Aspect @EnableAspectJAutoProxy(proxyTargetClass = true) public class LogAspect { }
当然,在新版 Spring Boot 项目中,有接口的类默认就是使用 CGLIB 动态代理的。但是此时如果有接口的类你又想使用 JDK 动态代理,那么可以通过如下配置:
spring.aop.proxy-target-class=false
关于 Spring Boot 中的 AOP 代理问题,可以参考去年松哥写的文章: Spring Boot 中的 AOP,到底是 JDK 动态代理还是 Cglib 动态代理?。
2. 实际用的类
基于第一小节的讲解,小伙伴们知道,当你在项目中用到了 AOP 之后,其实你所以见到的类,并不是原本的类了。
松哥前面写了好几篇 AOP 相关的文章,如下:
虽然是解决不同的问题,但是有一个共同的点,那就是都是通过自定义注解+ AOP 解决问题的。
现在我就以手把手教你玩多数据源动态切换!为例,来和大家说说这里的动态代理到底是咋回事,没看过这篇文章的小伙伴可以先看下。
小伙伴们看下,我的 UserService 大致上是下面这样:
@Service public class UserService { @Autowired UserMapper userMapper; @DS("master") public Integer count() { return userMapper.getCount(); } }
小伙伴们看到,count() 方法上加了 @DS 注解,所以这个 count() 方法将来是要被自动代理的。换言之,当你在另外一个类中注入 UserService 的时候,其实不是这个 UserService,我 DEBUG 小伙伴们来看一下:
小伙伴们从图中可以看到,此时我注入的 UserService 并不是真正的 UserService,而是一个通过 CGLIB 动态代理为 UserService 生成的子类,这个子类里边的 count 方法大致逻辑类似下面这样(其实就是 AOP 中的代码,具体小伙伴们可以参考 手把手教你玩多数据源动态切换!一文):
# 切换数据源 # 去数据库查询 count # 清空 ThreadLocal 中的变量 # ...
但是,如果我的调用逻辑是这样呢:
@Service public class UserService { @Autowired UserMapper userMapper; public Integer count2() { return count(); } @DS("master") public Integer count() { return userMapper.getCount(); } }
小伙伴们来看,count2 方法,这个时候直接在 count2 方法中调用了 count 方法,当然,count2() 方法中的调用也可以写作 this.count();
,这样看起来就更明确了,我们调用 count 方法,使用的是当前对象,而当前对象是不包含代理对象中的代码的,我们通过 DEBUG 来看下:
所以,当我们在 count2 中直接调用 count 方法的时候,那么加在 count 方法上的注解就会失效。
3. 问题解决
这个问题存在于所有使用了 AOP 的地方,存在的原因第二小节已经分析的很清楚了。
解决办法其实也有很多种,最为简单省事的一种,就是在当前类中注入代理对象,然后通过代理对象去调用其他方法,如下:
@Service public class UserService { @Autowired UserMapper userMapper; @Autowired UserService userService; public Integer count2() { return userService.count(); } @Transactional @DS("master") public Integer count() { return userMapper.getCount(); } }
虽然问题解决了,不过这毕竟不是一个好的解决办法(因为自己中注入自己,在新版 Spring Boot 中要开启循环依赖才能实现),大家在实际开发中,还是要从设计上尽量避免这种问题。
好啦,这个问题搞明白了,那么事务失效这个问题,也不用我多说了吧!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
RocketMQ之消费者启动与消费流程
vivo 互联网服务器团队 - Li Kui 一、简介 1.1 RocketMQ 简介 RocketMQ是由阿里巴巴开源的分布式消息中间件,支持顺序消息、定时消息、自定义过滤器、负载均衡、pull/push消息等功能。RocketMQ主要由 Producer、Broker、Consumer 、NameServer四部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。NameServer充当名字路由服务,整体架构图如下所示: Producer:负责生产消息,一般由业务系统生产消息,可通过集群方式部署。RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要。 Consumer:负责消费消息,一般是后台系统负责异步消费,可通过集群方式部署。一个消息消费者会从Broker服务器拉取消息、并将其提供给应用程序。提供pull/push两者消费模式。 Broker Server:负责存储消息、转发消息。RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消...
- 下一篇
解读Go分布式链路追踪实现原理
摘要:本文将详细介绍分布式链路的核心概念、架构原理和相关开源标准协议,并分享我们在实现无侵入 Go 采集 Sdk 方面的一些实践。 本文分享自华为云社区《一文详解|Go 分布式链路追踪实现原理》,作者:开源小E。 在分布式、微服务架构下,应用一个请求往往贯穿多个分布式服务,这给应用的故障排查、性能优化带来新的挑战。分布式链路追踪作为解决分布式应用可观测问题的重要技术,愈发成为分布式应用不可缺少的基础设施。本文将详细介绍分布式链路的核心概念、架构原理和相关开源标准协议,并分享我们在实现无侵入 Go 采集 Sdk 方面的一些实践。 为什么需要分布式链路追踪系统 微服务架构给运维、排障带来新挑战 在分布式架构下,当用户从浏览器客户端发起一个请求时,后端处理逻辑往往贯穿多个分布式服务,这时会浮现很多问题,比如: 请求整体耗时较长,具体慢在哪个服务? 请求过程中出错了,具体是哪个服务报错? 某个服务的请求量如何,接口成功率如何? 回答这些问题变得不是那么简单,我们不仅仅需要知道某一个服务的接口处理统计数据,还需要了解两个服务之间的接口调用依赖关系,只有建立起整个请求在多个服务间的时空顺序,才能更...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS关闭SELinux安全模块
- Hadoop3单机部署,实现最简伪集群
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2配置默认Tomcat设置,开启更多高级功能