Spring AOP、代理与声明式事务
为什么在一个方法上写了 @Transactional,有时却没有开启预期的事务?关键在于调用有没有经过 Spring 创建的代理。下面先画清普通对象与代理的关系,再看事务传播和异常处理。
Spring AOP 通过代理拦截 Spring Bean 的方法执行,声明式事务是其最重要应用之一。理解代理边界才能解释事务为何有时“失效”。
先把代理想成方法入口处的一层包装
假设下单需要同时保存订单和扣减库存。业务方法负责两次写入,代理可以在进入方法前开启事务,在正常结束后提交,在符合回滚规则的失败后回滚。这让业务代码少写重复的事务管理步骤,但事务范围依然需要你设计。
@Transactional 本身只是一份说明,运行时必须有对应基础设施处理它。默认代理模式下,调用要经过代理才能触发拦截;同一个对象内部调用另一个带注解的方法,不会因此再经过代理。排查时先画出调用方、代理、目标对象三者,而不是只检查注解拼写。
也别把事务理解为“发生任何错误,所有系统自动恢复”。本地数据库事务通常不包含已经发出去的 HTTP 请求。涉及消息时,要区分普通提交后回调和可以持久重试的 Outbox;进程在提交后崩溃,普通回调可能还没执行。方法可见性、代理类型及回滚规则见 Spring 官方说明。
1. 本文覆盖范围
- join point、pointcut、advice、aspect 与 advisor
- JDK/CGLIB 类代理与 self-invocation
- @Transactional 传播、隔离、回滚和只读
- 事务事件、远程调用边界与测试
2. 核心知识详解
1. Spring AOP 模型
Spring AOP 只支持 Spring Bean 方法执行 join point,以 JDK 动态代理或类代理包装目标。before/after/around advice 通过 pointcut 选择方法。
- 横切关注点适合日志、指标、授权和事务,不承载核心业务流程。
- pointcut 尽量精确,避免无意代理整个应用。
- around advice 自己决定是否调用
proceed():普通包装通常调用一次,缓存命中可以不调用,重试可能调用多次。每一种都要明确返回值、副作用和异常语义,不能把“必须调用一次”当作 AOP 的通用规则。参见 Declaring Advice。
这里容易混淆的是: Spring AOP 不是完整 AspectJ;字段访问和任意对象构造等 join point 不在代理式 AOP 范围。
2. 代理与自调用
外部调用经过代理才能触发 advice;目标对象内部 this.otherMethod() 不经过代理,因此该方法上的事务/缓存/异步注解通常不会生效。
- 把事务边界放在外部应用服务公开方法。
- 需要复用时拆分到另一个 Bean,而非获取当前代理形成隐式耦合。
- final/private 方法不适合作为类代理拦截点。
这里容易混淆的是: 注解只是元数据,真正行为来自代理和 advisor;对象不由 Spring 管理时注解没有对应拦截器。
3. 事务传播与回滚
REQUIRED 加入现有事务或新建,REQUIRES_NEW 挂起外层并新建,NESTED 依赖 savepoint 支持。默认对 RuntimeException/Error 回滚,受检异常需通过 rollbackFor 或显式策略。
- 事务方法围绕一个本地数据库一致性边界。
- 捕获异常后若继续提交,要明确业务允许;否则重新抛出或标记 rollback-only。
- REQUIRES_NEW 需要额外连接,嵌套使用可能耗尽连接池。
这里容易混淆的是: readOnly=true 是优化提示和框架语义,不是数据库权限控制。
4. 远程调用与事务事件
数据库事务上下文不会自动跨 HTTP/RPC 传播。事务内调用远端会延长锁持有并产生“本地回滚、远端已成功”。可靠事件使用 outbox 或事务后回调。
- @TransactionalEventListener 按阶段执行,但进程崩溃下并不等于可靠消息。
- 事件必须有幂等 key、版本和重放策略。
- 集成测试验证真实数据库提交/回滚而非只 Mock。
这里容易混淆的是: 本地事务 + 发送消息的普通两步操作不是原子事务。
3. 工程链路
4. 教学示例(结合本章运行前提)
这段代码假设 accounts 已作为依赖注入。重点不在两个方法名,而在扣款与入账是否处于同一数据库事务,以及调用是否经过代理。还要测试第二步抛出异常的情况,并按异常类型确认回滚规则。
@Service
class TransferService {
@Transactional
public void transfer(long from, long to, BigDecimal amount) {
accounts.debit(from, amount);
accounts.credit(to, amount);
}
}
// 事务方法应从代理外部调用;同类自调用不会经过代理拦截。
5. 实践与验证
- 制造 self-invocation 导致事务未生效,再通过服务拆分修复。
- 验证 REQUIRED/REQUIRES_NEW 在内外层异常组合下的提交结果。
- 实现 transactional outbox 并测试进程重启后的重放。
6. 掌握检查
- 能解释代理式 AOP 的 join point 限制。
- 能说明事务注解失效的常见原因。
- 能选择传播和回滚规则。
- 能划清本地事务与远程调用边界。