Appearance
Spring AOP
AOP 负责处理横切逻辑。横切逻辑是指那些不属于某个业务动作本身,却在很多业务动作前后都要重复发生的处理,例如事务、日志、审计、权限、限流、幂等、性能统计。
Spring AOP 的实现方式是代理。调用方拿到的是代理对象,代理对象先执行通用逻辑,再把调用交给真实业务对象。可以把它理解成进入方法前的一道门禁:该检查的检查,该记录的记录,该开启事务的开启事务,然后才放行到真正的方法。

总结图把事务失效现象归到代理入口、Advisor 匹配、线程资源和异常规则四类证据。判断事务结果之前,应先证明调用确实进入了代理和 TransactionInterceptor。
配套视频:EP17:Advisor 怎样匹配 Bean 并生成代理 解释代理形成;EP18:
@Transactional怎样提交或回滚 解释一次事务调用。
详细位置
- 本页说明 AOP 的适用边界、代理机制、事务失效、切点设计和排查方式。
- Bean 创建与注入见 IoC 容器。
- HTTP 请求进入 Service 前的链路见 Spring MVC。
- 自动配置与启动装配见 Spring Boot。
适用边界
适合 AOP 的逻辑通常有三个特征:
- 重复出现:很多方法都需要同类处理。
- 与业务主线可分离:去掉后,业务流程仍然能被清楚描述。
- 触发规则稳定:可以通过注解、包路径、类名或方法签名准确识别。
不适合 AOP 的逻辑:
- 关键业务分支,例如订单是否可取消、库存是否可扣减。
- 只服务于一两个方法的普通步骤。
- 会改变调用者对方法语义理解的隐藏逻辑。
- 只能依赖脆弱命名规则匹配的方法。
判断方式:如果这段逻辑写成 AOP 后,读业务方法的人会误判业务结果,就不应该用 AOP。
基本术语
| 术语 | 含义 | 直观理解 |
|---|---|---|
| Aspect | 切面 | 一组横切逻辑 |
| Pointcut | 切点 | 哪些方法会被拦住 |
| Advice | 通知 | 拦住之后执行什么 |
| Target | 目标对象 | 真正处理业务的对象 |
| Proxy | 代理对象 | 调用方实际访问的外层对象 |
| Join Point | 连接点 | Spring AOP 中主要是方法执行点 |
业务开发中最常见的是“用注解标记方法,再由切面处理”。例如某个方法需要审计,就加审计注解;某个方法需要幂等,就加幂等注解。注解是业务代码和横切逻辑之间的契约。
代理机制
代理机制的关键不是“JDK 动态代理还是 CGLIB”,而是“调用有没有经过代理对象”。
会经过代理的调用:
- Controller 调用容器注入的 Service。
- 一个 Service 调用另一个容器注入的 Service。
- 外部调用从 Spring 容器拿到的 Bean。
不会经过代理的调用:
- 对象内部使用
this调用自己的另一个方法。 - 手动
new出来的对象。 - 私有方法、构造方法这类不是外部代理入口的方法。
这就是很多 @Transactional、缓存注解、自定义切面失效的根本原因:注解在方法上,但调用没有走到代理门口。
代理怎样形成
Bean 创建接近完成时,AbstractAutoProxyCreator 会检查当前 Bean 是否匹配 Advisor。匹配成功后,它把目标对象、拦截器链和代理策略交给 ProxyFactory,最终返回代理对象放入单例池。后续其他 Bean 注入到的通常是这个代理,而不是最初实例化的对象。
| 方式 | 常见选择条件 | 需要注意 |
|---|---|---|
| JDK 动态代理 | 目标对象通过接口暴露能力 | 代理类型面向接口,直接按实现类取对象可能产生误解 |
| CGLIB 类代理 | 没有合适接口或明确使用类代理 | final 类或 final 方法不能被子类覆盖增强 |
选择哪一种代理不是排查的第一步。第一步仍然是确认调用方拿到的对象是否为代理,以及这次调用是否从代理外部进入。
事务与 AOP
@Transactional 可以看作 Spring 提供的内置切面。它在方法调用前开启事务,在方法正常返回后提交事务,在异常符合回滚规则时回滚事务。
事务失效常见原因:
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 同类内部调用事务方法不生效 | this 调用绕过代理 | 把事务边界放到外部入口,或拆到另一个 Service |
| 异常发生但没有回滚 | 异常被捕获后吞掉 | 记录后继续抛出,或明确设置回滚规则 |
| 手动创建对象后事务不生效 | 对象不是 Spring Bean | 由容器注入对象 |
| 方法不可见或被限制 | 代理无法作为外部入口增强 | 把事务放在清晰的 public 应用服务方法上 |
事务边界通常放在应用服务层,而不是 Controller 或 Repository。Controller 太靠近协议,Repository 太靠近数据访问细节;Service 更适合表达“一次业务操作要保持一致”。
下面的写法看起来给 createOrder() 加了事务,但 placeOrder() 通过 this 直接调用目标方法,事务拦截器没有机会介入:
java
@Service
public class OrderService {
public void placeOrder(OrderCommand command) {
this.createOrder(command); // 绕过代理
}
@Transactional
public void createOrder(OrderCommand command) {
// 写订单、扣库存
}
}更清晰的设计是把事务放到外部可见的应用服务入口,调用方直接调用代理公开的方法;如果两个步骤确实需要不同事务边界,再拆成两个职责明确的 Bean。
事务内部发生了什么
TransactionInterceptor 在进入业务方法前取得 TransactionAttribute,选择 PlatformTransactionManager,并根据传播行为决定新建事务、加入现有事务或挂起现有事务。数据库事务还会把连接等资源绑定到当前线程,使同一调用链中的数据访问共享事务资源。
| 传播行为 | 当前已有事务 | 当前没有事务 | 常见用途 |
|---|---|---|---|
REQUIRED | 加入 | 新建 | 默认业务事务 |
REQUIRES_NEW | 挂起原事务并新建 | 新建 | 独立审计、必须单独提交的动作 |
SUPPORTS | 加入 | 非事务执行 | 可选事务的只读查询 |
MANDATORY | 加入 | 抛出异常 | 强制要求上层提供事务 |
传播行为描述的是调用边界,不是数据库隔离级别。隔离级别处理并发读写可见性,传播行为处理方法调用时如何使用事务,两者不要混用。
一次事务调用的完整执行过程
当 Controller 调用 orderService.create() 时,首先进入代理而不是目标对象。代理按顺序执行 Advisor 对应的拦截器,TransactionInterceptor 从方法、目标类和注解中解析 TransactionAttribute,再根据限定符或默认规则选择 PlatformTransactionManager。对于 JDBC,事务管理器从 DataSource 取得连接,关闭自动提交,并把连接持有者绑定到当前线程。
之后目标方法调用 Repository。Repository 再通过 Spring 的数据访问设施取得连接时,会复用当前线程已经绑定的资源,而不是另开一个与事务无关的连接。业务方法正常返回,拦截器提交物理事务并解绑、释放资源;异常符合回滚规则时则回滚。@Transactional 能覆盖多次数据库操作,不是因为注解传播到了 Repository,而是因为同一调用线程上的数据访问共享了事务资源。
这也给出了三个清晰边界。异步线程不会天然继承原线程的事务;跨 HTTP 或消息调用的远端服务不会加入本地数据库事务;手动使用不受 Spring 管理的数据源连接,也不会自动复用线程绑定资源。声明式事务解决的是一个进程内、一个事务管理器所管理资源的调用边界,不是分布式一致性的通用答案。
REQUIRED 内层失败为什么可能在外层提交时报错
假设 OrderService.place() 开启 REQUIRED 事务,并调用同样使用 REQUIRED 的 InventoryService.deduct()。内层加入的是同一个物理事务。如果库存更新抛出运行时异常,事务会被标记为 rollback-only。即使外层捕获异常并继续返回,外层最终请求提交时也不能把一个已标记回滚的事务提交,于是会得到 UnexpectedRollbackException。
java
@Transactional
public void place(Order order) {
orderRepository.save(order);
try {
inventoryService.deduct(order.items());
} catch (InsufficientStockException ex) {
log.warn("deduct failed", ex); // 捕获异常并不会清除 rollback-only
}
}正确处理取决于业务语义:订单和库存必须原子成功,就让异常继续传播并整体回滚;允许库存失败但订单保留,就需要重新设计状态机和补偿,而不是简单吞异常;确实需要独立提交的审计可使用独立 Bean 上的 REQUIRES_NEW,但要考虑额外连接和内外事务提交顺序。传播行为不是“遇到异常换个注解”的修补工具,它表达的是业务动作之间是否共享同一个提交命运。
回滚规则不是“发生异常就回滚”
默认声明式事务通常对 RuntimeException 和 Error 回滚,对受检异常不自动回滚。更关键的是,事务拦截器只能处理从代理调用中传播出来的异常。业务方法在内部捕获异常并正常返回,代理看到的就是成功;异常在事务方法提交后才发生,例如消息异步发送失败,也不会倒推已经提交的数据库事务。
设计事务方法时,应同时写清数据库修改范围、异常传播方式、外部副作用和重试语义。把远程支付、发消息和数据库写入全部包进长事务,既不能让远端操作参与本地回滚,还会长时间占用连接。常见做法是先在短事务中保存业务状态和事件记录,再由事务外的可靠投递机制处理远端副作用。
切点设计
切点设计的目标是“准”。过宽会误伤,过窄会漏掉。
常见方式:
| 方式 | 适合场景 | 风险 |
|---|---|---|
| 按注解匹配 | 审计、幂等、操作日志、业务开关 | 需要开发者显式标记 |
| 按包路径匹配 | 某一层统一治理,例如 Service 层日志 | 包结构变动会影响范围 |
| 按方法签名匹配 | 命名规范非常稳定的项目 | 命名变化可能误伤或漏掉 |
| 组合切点 | 需要精确限定范围 | 表达式复杂后可读性下降 |
优先使用注解表达业务意图。比如“这个方法需要审计”比“所有 save* 方法都审计”更稳定。
通知类型
| 通知 | 执行时机 | 适合做什么 |
|---|---|---|
| 前置通知 | 方法执行前 | 权限检查、参数快速校验 |
| 后置通知 | 方法结束后,无论成功失败 | 清理上下文 |
| 返回通知 | 方法正常返回后 | 成功审计、成功指标 |
| 异常通知 | 方法抛出异常后 | 异常统计、失败审计 |
| 环绕通知 | 包住整个调用 | 事务、耗时、限流、幂等 |
能用更窄通知时,不要默认使用环绕通知。环绕通知能力最大,也最容易隐藏调用行为。
工程场景
操作审计
审计记录“谁在什么时候做了什么”。它适合 AOP,因为审计不应混进每个业务方法的主流程。业务方法只标记需要审计,切面统一读取用户、动作、结果和异常。
需要注意:
- 审计失败是否影响业务成功,要有明确规则。
- 审计内容不要记录敏感明文,例如密码、完整 Token。
- 审计切点应尽量用注解,不要靠方法名猜测。
幂等控制
幂等控制用于防止重复提交。它适合 AOP 的前提是 Key 规则清晰、失败处理清晰、重复请求返回策略清晰。
需要注意:
- Key 应来自业务唯一信息,例如订单号、请求号、用户 ID 和动作组合。
- 占位或锁必须有过期时间。
- 方法失败后是否释放 Key,要按业务语义决定。
- 幂等不是简单加锁,它影响用户重复请求的最终语义。
日志与耗时
日志和耗时统计适合 AOP,但应该克制:
- 不要打印大对象和敏感字段。
- 不要在高频方法上做复杂序列化。
- 日志要包含可关联的请求 ID 或业务 ID。
- 耗时统计应能区分接口耗时、数据库耗时、外部调用耗时。
排查路径
注解不生效
按顺序检查:
- 目标类是否是 Spring Bean。
- 调用方拿到的是否是代理对象。
- 调用是否从外部进入,而不是
this内部调用。 - 切点是否能匹配方法。
- 方法可见性和类结构是否适合代理。
- 异常是否按预期继续传播。
切面执行顺序不对
多个切面同时命中时,应显式设置顺序。通常权限、限流、幂等这类“可能拒绝调用”的逻辑更靠前;事务包住需要一致性的业务操作;日志和指标负责记录结果。
性能异常
检查切点是否过宽,是否在高频方法中做了参数深拷贝、JSON 序列化、远程写日志等重操作。AOP 越靠底层、命中越频繁,越需要控制成本。
源码与运行验证
排查代理问题时,可以按下面的证据顺序推进:
- 打印
bean.getClass(),确认实际类型是否包含代理特征。 - 使用
AopUtils.isAopProxy(bean)、isJdkDynamicProxy()、isCglibProxy()判断代理类型。 - 在
AbstractAutoProxyCreator.wrapIfNecessary()查看 Bean 是否匹配 Advisor。 - 在
TransactionInterceptor.invoke()查看事务调用是否真正进入拦截器。 - 检查日志中的事务开始、提交或回滚是否与业务异常一致。
不要用“方法上有注解”作为事务已生效的证据。代理类型、拦截器断点、数据库结果和事务日志才能形成完整闭环。
总结
AOP 的核心是代理边界。横切逻辑应该像方法外层的统一检查站,而不是隐藏业务流程的暗门。排查时先确认对象是否由容器管理,再确认调用是否经过代理,最后检查切点、顺序、异常传播和性能成本。
