Spring 事务失效了,怎么办?

 2023-01-09
原文作者:江南一点雨 原文地址:https://juejin.cn/post/7119302593163051038

这是小伙伴们在微信上问的一个问题:

202301012015460091.png

这个问题比较典型,让我想到面试时有一个 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 小伙伴们来看一下:

202301012015466382.png

小伙伴们从图中可以看到,此时我注入的 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 来看下:

202301012015471293.png

所以,当我们在 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();
        }
    
    }

202301012015475854.png

虽然问题解决了,不过这毕竟不是一个好的解决办法(因为自己中注入自己,在新版 Spring Boot 中要开启循环依赖才能实现),大家在实际开发中,还是要从设计上尽量避免这种问题。

好啦,这个问题搞明白了,那么事务失效这个问题,也不用我多说了吧!