Spring Boot 从切面统一控制事务:告别满屏 @Transactional 的实战方案
本文用一个自定义 AOP 切面接管 Spring Boot 项目里所有 Service 层的事务控制,覆盖注解驱动与包级切点两种实现、切面排序、只读事务、回滚规则,最后附 7 个真实踩坑。代码基于 Spring Boot 3.x / 4.x(jakarta 命名空间),可直接复制运行。
前言
@Transactional 用久了,几个典型痛点一定会冒出来:
- 漏标注解 :新同事写了个 Service 方法忘了加
@Transactional,上线后一半数据写进去了,一半没有; - 回滚规则不统一 :有人写了
rollbackFor = Exception.class,有人没写,受检异常的回滚行为各处不一致; - 代码侵入:业务方法上挂满事务属性,事务策略散落在几十个注解里,想统一调整传播行为、超时时间要改 N 处。
一个更工程化的思路是:把事务控制从业务代码里抽出来,交给一个统一的 AOP 切面。本文就来完整实现这个方案。
先说清楚一件事:Spring 的声明式事务(@Transactional)底层本来就是 AOP 实现的。我们要做的,本质上是手写一个"事务切面",把事务策略从注解分散配置收敛为切面里的集中配置。
一、核心原理
自定义事务切面的工作模型:
text
调用方 → 代理对象 → 自定义事务切面(@Around)
│ 1. 开启事务(getTransaction)
│ 2. 执行目标方法 proceed()
│ 3a. 正常返回 → commit
│ 3b. 抛出异常 → rollback
└→ 目标 Service 方法
切面里操作事务有两个主流工具:
| 工具 | 用法 | 适用场景 |
|---|---|---|
PlatformTransactionManager |
getTransaction() / commit() / rollback() 手动控制 |
需要精细控制事务生命周期 |
TransactionTemplate |
execute(status -> {...}) 回调式 |
Spring 官方推荐的编程式事务写法,代码更简洁 |
Spring 官方文档明确建议:命令式流程的编程式事务管理优先使用
TransactionTemplate。本文两种写法都会给出。
二、环境准备
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!-- 3.5.x 是 3.x 系列最后一个开源维护线;新项目可直接上 4.1.x(基于 Spring Framework 7) -->
<version>3.5.16</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- AOP 支持:提供 AspectJ 注解能力 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
关键点:spring-boot-starter-aop 必须引,否则 @Aspect 不生效。引入 starter-data-jpa 后,Spring Boot 会自动配置 JpaTransactionManager(即 PlatformTransactionManager 的实现),切面里直接注入即可,不需要手写任何事务管理器配置。
三、方案一:自定义注解 + 切面(推荐)
思路:定义一个 @Tx 注解标记需要事务的方法,切面只拦带注解的方法,事务属性全部由注解参数控制。相比 @Transactional 的好处是:默认回滚规则由我们自己定,一次配置全局生效。
3.1 定义注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Tx {
/** 传播行为,默认 REQUIRED */
Propagation propagation() default Propagation.REQUIRED;
/** 隔离级别,默认用数据库自身的 */
Isolation isolation() default Isolation.DEFAULT;
/** 只读事务,查询方法设为 true 可让框架做优化 */
boolean readOnly() default false;
/** 超时时间(秒),默认不限制 */
int timeout() default -1;
/** 触发回滚的异常类型,默认所有 Exception(含受检异常) */
Class<? extends Throwable>[] rollbackFor() default {Exception.class};
/** 不回滚的异常类型,优先级高于 rollbackFor */
Class<? extends Throwable>[] noRollbackFor() default {};
}
3.2 切面实现(TransactionTemplate 版)
java
@Aspect
@Component
@Order(0) // 见第五节:事务切面的排序问题
public class TxAspect {
private final TransactionTemplate transactionTemplate;
public TxAspect(PlatformTransactionManager txManager) {
this.transactionTemplate = new TransactionTemplate(txManager);
}
@Around("@annotation(tx)")
public Object around(ProceedingJoinPoint pjp, Tx tx) throws Throwable {
// 1. 把注解参数映射到事务定义
transactionTemplate.setPropagationBehavior(tx.propagation().value());
transactionTemplate.setIsolationLevel(tx.isolation().value());
transactionTemplate.setReadOnly(tx.readOnly());
if (tx.timeout() > 0) {
transactionTemplate.setTimeout(tx.timeout());
}
// 2. 回调式执行:异常自动回滚,正常返回自动提交
try {
return transactionTemplate.execute(status -> {
try {
return pjp.proceed();
} catch (Throwable e) {
// 3. 按注解配置的回滚规则判断
if (shouldRollback(e, tx)) {
status.setRollbackOnly(); // 标记回滚
}
throw new RuntimeException(e); // 抛出去让 execute 感知异常
}
});
} catch (RuntimeException e) {
throw e.getCause() != null ? e.getCause() : e; // 还原原始异常
}
}
private boolean shouldRollback(Throwable ex, Tx tx) {
for (Class<? extends Throwable> noRollback : tx.noRollbackFor()) {
if (noRollback.isInstance(ex)) {
return false;
}
}
for (Class<? extends Throwable> rollback : tx.rollbackFor()) {
if (rollback.isInstance(ex)) {
return true;
}
}
return false;
}
}
关键行解释:
status.setRollbackOnly():不直接调 rollback,而是标记事务为回滚状态,TransactionTemplate退出回调时会执行回滚------这是回调式事务里唯一正确的回滚姿势;- 异常被包了一层
RuntimeException才能穿透 lambda,所以出口处要getCause()还原,否则调用方 catch 不到原始异常类型。
3.3 业务代码使用
java
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final StockService stockService;
// 构造器注入省略
@Tx(timeout = 10) // 事务策略一目了然,漏不了
public void createOrder(Order order) {
orderRepository.save(order);
stockService.deduct(order.getProductId(), order.getQuantity());
// 任一步抛异常,整体回滚
}
@Tx(readOnly = true)
public List<Order> listOrders(Long userId) {
return orderRepository.findByUserId(userId);
}
}
四、方案二:包级切点,全局兜底
如果团队约定"Service 层所有写方法默认要有事务",可以直接按包名切,一个注解都不用写:
java
@Aspect
@Component
@Order(0)
public class GlobalTxAspect {
private final PlatformTransactionManager txManager;
public GlobalTxAspect(PlatformTransactionManager txManager) {
this.txManager = txManager;
}
// 拦截 service 包下所有 public 方法
@Pointcut("execution(public * com.example.demo.service..*(..))")
public void serviceLayer() {}
@Around("serviceLayer()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 已有事务上下文(嵌套调用)时不重复开新事务
if (TransactionSynchronizationManager.isActualTransactionActive()) {
return pjp.proceed();
}
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 方法名约定:查询类方法走只读事务
if (isReadMethod(pjp.getSignature().getName())) {
def.setReadOnly(true);
}
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = txManager.getTransaction(def);
try {
Object result = pjp.proceed();
txManager.commit(status);
return result;
} catch (Throwable e) {
txManager.rollback(status);
throw e;
}
}
private boolean isReadMethod(String methodName) {
return methodName.startsWith("get") || methodName.startsWith("find")
|| methodName.startsWith("list") || methodName.startsWith("query")
|| methodName.startsWith("count");
}
}
关键行解释:
TransactionSynchronizationManager.isActualTransactionActive():判断当前线程是否已有活动事务,避免嵌套调用时重复开启/提交(REQUIRED 语义的手工实现);getTransaction/commit/rollback三件套是PlatformTransactionManager的标准编程式用法,控制粒度比TransactionTemplate更细。
两种方案怎么选?
| 维度 | 方案一(注解 + TransactionTemplate) | 方案二(包级切点 + 事务管理器) |
|---|---|---|
| 控制粒度 | 方法级,可按注解定制属性 | 包级统一策略 |
| 可读性 | 显式标注,意图清晰 | 约定式,需要团队共识 |
| 漏配置风险 | 忘加注解就没事务 | 全覆盖,不会漏 |
| 推荐场景 | 事务策略差异大的项目 | 事务策略高度统一的 CRUD 项目 |
我的建议 :方案一为主,个别需要全兜底的模块叠加方案二,并用 isActualTransactionActive() 防重复。
五、切面排序:最容易忽略的细节
自定义事务切面通常会和日志、鉴权等其他切面共存,执行顺序直接决定行为正确性:
java
@Aspect
@Component
@Order(0) // 数值越小,越靠外层
public class TxAspect { ... }
@Aspect
@Component
@Order(10) // 在事务切面内层执行
public class LogAspect { ... }
@Order 数值小的切面在外层,环绕通知的调用栈呈"洋葱模型":
text
TxAspect.before → LogAspect.before → 目标方法 → LogAspect.after → TxAspect.after(commit)
为什么事务切面要在最外层? 以操作日志为例:如果日志切面在外层、事务切面在内层,业务回滚后日志记录也跟着丢了;反过来,事务在外层,日志切面内发生的异常不影响事务判断,且日志写入可以走自己的 REQUIRES_NEW 事务。原则:事务边界应该包住一切需要"同生共死"的逻辑。
六、常见问题与避坑
坑一:同类内部方法调用,切面不生效
典型现象:createOrder() 内部调用本类的 saveDetail(),后者标了 @Tx 却没进事务。原因是切面基于代理,内部调用走的是 this 而非代理对象。解法 :拆到不同类中调用;或注入自身代理(@Lazy 注入 self)。
坑二:切面里 catch 了异常,事务不回滚
java
try {
Object result = pjp.proceed();
txManager.commit(status);
return result;
} catch (Exception e) {
log.error("出错了", e); // 只记日志没 rollback!
return null;
}
解法 :catch 块里必须先 txManager.rollback(status) 再决定异常是吞掉还是上抛;用 TransactionTemplate 则要保证异常能穿透回调。
坑三:受检异常默认不回滚
无论是 @Transactional 还是手写切面,Spring 默认只对 RuntimeException 和 Error 回滚。业务抛 IOException 这类受检异常时数据照样提交。解法 :本文的 @Tx 注解已把 rollbackFor 默认设为 Exception.class,这正是自定义切面的价值之一。
坑四:自定义切面与 @Transactional 混用,事务边界混乱
同一个方法既有 @Transactional 又被自定义切面拦截,会出现两层事务逻辑叠加,嵌套语义难以预测。解法 :项目内二选一,并在代码规范里写死;迁移期可用 isActualTransactionActive() 做兜底判断。
坑五:TransactionTemplate 共享实例的状态污染
TransactionTemplate 本身是线程安全的,但它的传播行为、只读等属性是实例级状态。如果多个方法并发调用同一个实例且属性不同,会互相覆盖。解法 :要么每个切点配置新建模板,要么像方案一那样在 @Around 入口按当前注解重设属性(注意高并发下仍有竞争,严谨做法是每次 new TransactionTemplate(txManager) 后配置)。
坑六:只读事务里执行写操作
readOnly = true 的事务里做 insert/update,部分数据库驱动会直接报错,部分会静默成功但拿不到优化收益。解法:只读标记只给纯查询方法;命名约定(get/find/list)要严格执行。
坑七:长事务把连接池拖垮
切面包住的方法里混入了 RPC 调用、文件上传等耗时 IO,事务期间数据库连接一直被占用。解法 :事务方法内禁止远程调用,先拿结果再进事务;必要时给 @Tx(timeout = n) 设超时兜底。
七、总结
- 自定义事务切面的本质 是手写 Spring 声明式事务的底层机制:
@Around+PlatformTransactionManager(或TransactionTemplate),把事务策略从分散的注解收敛到集中的切面配置。 - 两种落地方式 :注解驱动(
@Tx+ TransactionTemplate,粒度细、意图清晰)和包级切点(全局兜底、不会漏),可按项目约定组合使用。 - 切面排序 用
@Order控制,事务切面放最外层,让日志等横切逻辑跑在事务边界之内。 - 三个最高频的坑:同类自调用不走代理 、catch 异常忘了 rollback 、受检异常默认不回滚。
- 事务方法里保持纯粹:只做数据库操作,耗时 IO 挪到事务外,配合超时时间防止连接池耗尽。
参考资料:Spring Framework 官方文档 Programmatic Transaction Management、Aspect Oriented Programming with Spring