Spring @Transactional 你真的会用吗???

前置知识

使用@Transactional 注解会发生什么?

当使用 @Transactional 注解时,Spring 会扫描这些注解并配置 AOP 代理,以便在目标方法执行前后进行事务管理。具体过程如下:

  • 方法拦截:当目标方法被调用时,AOP 代理会拦截这个调用,然后根据事务的配置(如传播行为和隔离级别)来处理事务。
  • 事务管理:在方法执行前,代理会调用事务管理器开始一个新的事务;在方法执行成功后,它会提交事务;如果方法抛出异常,代理会回滚事务。

简单的一句话理解就是,通过目标对象的代理类,调用带有 @Transactional 注解的方法,进行事务的管理。

@Transactional 注解什么时候会失效?

@Transactional 确实好用又方便,但是有坑呀!!!

注解失效几种情况:其实还有很多,所以要小心了。

  1. 如果你在 同一个类内部调用一个带有 @Transactional 注解的方法 ,事务将不会生效,因为调用是通过 this 引用直接访问的,没有经过代理。
  2. @Transactional 没有指定 rollbackFor,默认只对 RuntimeException Error进行回滚 。比如 IOException,则不会回滚,除非你在注解中指定了 rollbackFor 属性。
  3. 如果在 @Transactional 方法内部捕获了异常 (使用 try-catch 语句),仅打日志,没有抛出异常,不会回滚事务,因为异常被捕获后不会传播到 Spring 的事务管理器。
  4. @Transactional 注解不能应用于 privatefinalstatic 方法,因为 Spring 无法通过代理在这些方法上应用 AOP。确保方法是 public 的。
  5. 事务传播行为:如果在方法中调用了另一个带有 @Transactional 注解的方法,并且它的传播行为是 Propagation.NOT_SUPPORTED 或者其他不支持当前事务的类型,那么当前事务将会失效。

好了,我们来详细地讲讲几种事务失效的情况。

1. 同一个类内部调用一个带有 @Transactional 注解的方法

比如:method1 调用了 method2 方法(method2 带有事务注解),此时事务不生效

typescript 复制代码
@Service
public class MyService {

private void method1() {
     method2();
     // ...
}
@Transactional
 public void method2() {
     //...
  }
}

那么怎么解决呢?

既然是 Spring 通过代理类来调用带有 @Transactional 注解的方法 的,那么我直接把 @Transactional 注解的方法 放到另一个类中,然后再调用这个类中的方法不就行了。 嘿嘿!!

typescript 复制代码
@Service
public class MyService {
@Autowired
private MyManager myManager;
    
private void method1() {
     myManager.method2();
     // ...
    }
}

@Service
public class MyManager {

    // 这个方法是事务性的方法
    @Transactional
    public void method2() {
    
    }
}

看到这里,你是不是想说这么麻烦,还要搞多一个类???

没事还有另一个更简单的方法:就是通过 AopContext 获取该类的代理对象

typescript 复制代码
@Service
public class MyService {

private void method1() {
    // 先获取该类的代理对象,然后通过代理对象调用 method2方法。
     ((MyService)AopContext.currentProxy()).method2(); 
     //......
}
@Transactional
 public void method2() {
     //......
  }
}

这样子是不是简单多了。

顺便提一嘴:

你要想通过 AopContext 获取该类的代理对象,记得在启动类上,加上 @EnableAspectJAutoProxy(exposeProxy = true)

typescript 复制代码
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }
}

你肯定又好奇,这个注解是干什么的?为什么还加上了 proxyTargetClass = true 属性呢?我来给你讲讲都是干什么的?

  • EnableAspectJAutoProxy 表示开启 AOP功能, 允许你定义切面(Aspect)和切点(Pointcut),使用AOP记得要开启
  • exposeProxy = true 在方法内部获取当前类代理对象 ,比如上面就是通过 AopContext 获取该类的代理对象
  • proxyTargetClass = true 表示强制使用 CGLIB 代理

我们都知道,在 Spring 中,事务管理可以使用两种代理方式:JDK 动态代理和 CGLIB 代理。具体使用哪种代理方式取决于几个因素,包括目标类是否实现了接口以及 Spring 的配置。

在 Spring Boot 中,默认情况下,会根据目标类的特性自动选择代理方式:

  • 如果目标类实现了接口,使用 JDK 动态代理。
  • 如果目标类没有实现接口,使用 CGLIB 代理。

这里 exposeProxy = true ,表示我强制使用的是 CGLIB 代理。至于 JDK 动态代理 和 CGLIB 代理 在这里我就不过多的介绍了,感兴趣的小伙伴可以自己去了解一下。

2. rollbackFor 没有指定对

没有指定 rollbackFor,默认只对RuntimeExceptionError进行回滚。比如 IOException,则不会回滚。

怎么解决?

简单,在注解上加上 rollbackFor = Exception.class 不就好了,异常类型捕获一个大的,更放心。。

typescript 复制代码
@Service
public class MyService {

private void method1() {
     method2();
     // ...
}
@Transactional(rollbackFor = Exception.class)
 public void method2() {
     //...
  }
}

3. 你捕获异常,你又不抛出来,Spring 怎么知道出异常了,怎么回滚??

比如:@Transactional 方法内部捕获了异常 ,仅打日志,没有抛出异常。不会回滚事务,因为异常被捕获后不会传播到 Spring 的事务管理器。****

typescript 复制代码
@Service
@Slf4j
public class UserService {

    @Transactional
    public void userRegister(String username) {
        try {
            // ..... 
            
            // 模拟抛出一个异常
            if (username == null) {
                throw new IllegalArgumentException("用户名不可为空");
            }
        } catch (Exception e) {
        // 捕获了异常,仅打日志,没有抛出异常,不会回滚事务
            log.error("捕获异常: " + e.getMessage());
        }
    }
}

怎么解决?

直接抛出异常不就好了,这里其实我也有点纳闷,既然都出异常被捕获了,还打了日志,为什么不抛业务异常。我猜测又是哪一个粗心的小可爱忘了,一定不是你。嘿嘿!!

typescript 复制代码
@Service
@Slf4j
public class UserService {

    @Transactional
    public void userRegister(String username) {
        try {
           
            // .....
            
            // 模拟抛出一个异常
            if (username == null) {
                throw new IllegalArgumentException("用户名不可为空");
            }
        } catch (Exception e) {
        // 捕获了异常,仅打日志,没有抛出异常,不会回滚事务
            log.error("异常: " + e.getMessage());
            // 记得抛出异常
            throw new BusinessException("业务异常");
        }
    }
}

总结

今天我们讲了 Spring 事务失效的几种情况,也给出了解决的方案,最后希望使用 @Transactional 注解的时候要小心了,不要中招了。。。

好了,分享到结束了,如果觉得我写的还不错,记得给我三连哦,创作真的不容易,感谢大家的支持,谢谢!

相关推荐
小钟不想敲代码43 分钟前
第4章 Spring Boot自动配置
java·spring boot·后端
hummhumm1 小时前
第33章 - Go语言 云原生开发
java·开发语言·后端·python·sql·云原生·golang
AskHarries2 小时前
利用 OSHI获取机器的硬件信息
java·后端
凡人的AI工具箱2 小时前
40分钟学 Go 语言高并发:【实战】并发安全的配置管理器(功能扩展)
开发语言·后端·安全·架构·golang
我的运维人生2 小时前
Spring Boot应用开发实战:构建RESTful API服务
spring boot·后端·restful·运维开发·技术共享
颜淡慕潇2 小时前
【K8S系列】深入解析 Kubernetes 中的 Deployment
后端·云原生·容器·kubernetes
CopyLower3 小时前
深入理解 MyBatis 的缓存机制:一级缓存与二级缓存
spring·缓存·mybatis
黄昏_3 小时前
在Springboot项目中实现将文件上传至阿里云 OSS
java·spring boot·后端·阿里云
写bug写bug3 小时前
用Java Executors创建线程池的9种方法
java·后端