前置知识
使用@Transactional
注解会发生什么?
当使用 @Transactional
注解时,Spring 会扫描这些注解并配置 AOP 代理,以便在目标方法执行前后进行事务管理。具体过程如下:
- 方法拦截:当目标方法被调用时,AOP 代理会拦截这个调用,然后根据事务的配置(如传播行为和隔离级别)来处理事务。
- 事务管理:在方法执行前,代理会调用事务管理器开始一个新的事务;在方法执行成功后,它会提交事务;如果方法抛出异常,代理会回滚事务。
简单的一句话理解就是,通过目标对象的代理类,调用带有 @Transactional 注解的方法,进行事务的管理。
@Transactional 注解什么时候会失效?
@Transactional
确实好用又方便,但是有坑呀!!!
注解失效几种情况:其实还有很多,所以要小心了。
- 如果你在 同一个类内部调用一个带有
@Transactional
注解的方法 ,事务将不会生效,因为调用是通过this
引用直接访问的,没有经过代理。 @Transactional
没有指定 rollbackFor,默认只对RuntimeException
和Error
进行回滚 。比如IOException
,则不会回滚,除非你在注解中指定了rollbackFor
属性。- 如果在
@Transactional
方法内部捕获了异常 (使用try-catch
语句),仅打日志,没有抛出异常,不会回滚事务,因为异常被捕获后不会传播到 Spring 的事务管理器。 @Transactional
注解不能应用于private
或final
或static
方法,因为 Spring 无法通过代理在这些方法上应用 AOP。确保方法是public
的。- 事务传播行为:如果在方法中调用了另一个带有
@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,默认只对RuntimeException
和Error
进行回滚。比如 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 注解的时候要小心了,不要中招了。。。
好了,分享到结束了,如果觉得我写的还不错,记得给我三连哦,创作真的不容易,感谢大家的支持,谢谢!