使用springboot时,只要引入spring-jdbc/jpa相关的依赖后,在想要启用事务的方法上加上@Transactional注解就能开启事务,碰到异常就能自动回滚。大大的提高了编码的便捷性性,同时也不侵入代码,保持了代码的简洁性。
默认情况下,Spring时使用的Spring AOP (mode=Mode.Proxy, proxyTargetClass=false)方式启动数据库事务拦截。只有了解清楚了具体背景,才能清除知道事务为什么在碰到异常时没有能够正确回滚。下面是一些常用场景分析:
使用springboot开发时,引入以下依赖后通常会自动启用TransactionManager。
spring-boot-starter-jdbc
是 Spring Boot 提供的用于简化 JDBC(Java Database Connectivity)开发的启动器,引入该依赖后,Spring Boot 会自动配置 DataSourceTransactionManager
。
spring-boot-starter-data-jpa
是 Spring Boot 提供的用于简化 JPA(Java Persistence API)开发的启动器,它集成了 Hibernate 等 JPA 实现框架,方便开发者进行数据库操作。引入该依赖后,Spring Boot 会自动配置 JpaTransactionManager
。
通过下面代码中的printTransactionManager(TransactionManager transactionManager) 方法可以检查是否配置正常。
@SpringBootApplication
public class MybatisApplication {
public static void main(String[] args) {
org.springframework.boot.SpringApplication.run(MybatisApplication.class, args);
}
@Bean
Object printTransactionManager(TransactionManager transactionManager) {
System.out.println("transactionManager: " + transactionManager);
return null;
}
}
transactionManager: org.springframework.jdbc.support.JdbcTransactionManager@590765c4
通过打印语句,可以看到spring中的TransactionManager是否正确配置。
默认情况下,事务是在proxy模式下(即Spring AOP负责拦截事务),proxyTargetClass=false ,有接口的时候使用JDK动态代理实现。没有接口时使用CGLIB进行代理。
JDK代理接口时,都是public方法。
CGLIB代理时,在public方法上能生效。在Spring 6.0 以后,除public方法外,可以代理protected, package-visable修饰的方法。
详情参考文档:
Method visibility and
@Transactional
in proxy modeThe
@Transactional
annotation is typically used on methods withpublic
visibility. As of 6.0,protected
or package-visible methods can also be made transactional for class-based proxies by default. Note that transactional methods in interface-based proxies must always bepublic
and defined in the proxied interface. For both kinds of proxies, only external method calls coming in through the proxy are intercepted.
Using @Transactional :: Spring Framework
示例代码:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
private void createUser(User user) {
// 插入用户数据
userRepository.save(user);
// 可能会抛出异常
if (user.getName().equals("error")) {
throw new RuntimeException("创建用户失败");
}
}
}
因为方法的可见性,private修饰的方法不能被代理拦截
@Transactional
注解使用 AOP 实现事务管理,而 AOP 是基于代理模式的。当在同一个类内部一个没有事务注解的方法调用有 @Transactional
注解的方法时,事务注解会失效,因为这种调用没有经过代理对象。
示例代码如下:
// 定义 UserService 类,处理用户相关业务逻辑
@Service
class UserService {
@Autowired
private UserRepository userRepository;
// 无事务注解的方法,内部调用有事务注解的方法
public void outerMethod() {
try {
innerMethod();
} catch (Exception e) {
System.out.println("Exception caught: " + e.getMessage());
}
}
// 有事务注解的方法
@Transactional
public void innerMethod() {
User user = new User("John");
userRepository.save(user);
// 模拟抛出异常
throw new RuntimeException("Simulated exception");
}
}
示例代码中,虽然Spring AOP代理了innerMethod方法,但是原始事务不是通过代理的innerMethod进入,而是通过原始类的outerMethod进入,这样就调用的是原始类的innerMethod方法,导致不能进入代理类的innerMethod方法,事务拦截不能生效。
在 Spring 中使用 @Transactional
注解时,如果在方法内部捕获了异常且没有重新抛出,会导致事务无法正常回滚,从而使 @Transactional
注解失效。
@Transactional
注解的事务管理是基于 AOP 实现的,它会在目标方法抛出异常时进行事务回滚。默认情况下,@Transactional
注解只对未被捕获的 RuntimeException
及其子类异常进行回滚操作。如果在方法内部捕获了异常,Spring 就无法感知到异常的抛出,从而不会触发事务回滚逻辑,导致事务继续提交,@Transactional
注解的功能失效。
示例代码
// 定义 UserService 类,处理用户相关业务逻辑
@Service
class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void createUser() {
User user = new User("John");
userRepository.save(user);
try {
// 模拟抛出异常
throw new RuntimeException("Simulated exception");
} catch (Exception e) {
// 捕获异常但未重新抛出
System.out.println("Exception caught: " + e.getMessage());
}
}
}
createUser()
方法:使用 @Transactional
注解标记,在方法内部保存用户信息后抛出一个 RuntimeException
异常,并在 catch
块中捕获该异常,但没有重新抛出。
解决办法:重新抛出异常
在 catch
块中重新抛出异常,让 Spring 能够感知到异常的发生,从而触发事务回滚逻辑。
@Transactional
public void createUser() {
User user = new User("John");
userRepository.save(user);
try {
throw new RuntimeException("Simulated exception");
} catch (Exception e) {
System.out.println("Exception caught: " + e.getMessage());
// 重新抛出异常
throw e;
}
}
默认回滚规则:@Transactional
注解默认只对RuntimeException
及其子类和Error
进行回滚。若抛出的是受检查异常(如IOException
、SQLException
),默认不会触发回滚。
@Service
class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void createUser() throws IOException {
User user = new User("John");
userRepository.save(user);
// 抛出受检查异常
throw new IOException("Simulated IOException");
}
}
IOException是一个CheckedException的子类,不在默认的回滚体系内,所以不能自动回滚。需要使用rollbackFor属性显式指定才能生效。
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Transactional(rollbackFor = BaseException.class)
public void createOrder(Order order) throws Exception {
orderRepository.createOrder(order);
// will rollback
// throw new SubException("出现未知错误");
// will not rollback
throw new OtherException("出现未知错误");
}
}
class BaseException extends Exception {
public BaseException(String message) {
super(message);
}
}
class SubException extends BaseException {
public SubException(String message) {
super(message);
}
}
class OtherException extends Exception {
public OtherException(String message) {
super(message);
}
}
rollbackForClassName=exceptionPattern, exceptionPattern可以包含异常名字全部或者部分字符。注意这里不是正则表达式,而是基于String.contains(exceptionPattern)来判断的。
匹配规则的源码如下:
private int getDepth(Class> exceptionType, int depth) {
if (this.exceptionType != null) {
if (this.exceptionType.equals(exceptionType)) {
// Found it!
return depth;
}
}
else if (exceptionType.getName().contains(this.exceptionPattern)) {
// Found it!
return depth;
}
// If we've gone as far as we can go and haven't found it...
if (exceptionType == Throwable.class) {
return -1;
}
return getDepth(exceptionType.getSuperclass(), depth + 1);
}
不能匹配的示例代码如下:
@Slf4j
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Transactional(rollbackForClassName = "Base*Exception")
public void createOrder(Order order) throws Exception {
orderRepository.createOrder(order);
// will not rollback
throw new Base1Exception("出现未知错误");
}
}
因为exception.typeName="Base1Exception" contains("Base*Exception") 结果未false,所以不匹配,导致不能回滚。
正确的用法如下:
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Transactional(rollbackForClassName = "Base")
public void createOrder(Order order) throws Exception {
orderRepository.createOrder(order);
// will rollback
throw new Base1Exception("出现未知错误");
}
}
以上是我过去经常碰到的@Transactional碰到异常不能正常回滚的案例总结,若有遗漏欢迎下方留言。
参考文档:
Declarative Transaction Management :: Spring Framework