Appearance
Spring AOP
AOP 负责处理横切逻辑。横切逻辑是指那些不属于某个业务动作本身,却在很多业务动作前后都要重复发生的处理,例如事务、日志、审计、权限、限流、幂等、性能统计。
Spring AOP 的实现方式是代理。调用方拿到的是代理对象,代理对象先执行通用逻辑,再把调用交给真实业务对象。可以把它理解成进入方法前的一道门禁:该检查的检查,该记录的记录,该开启事务的开启事务,然后才放行到真正的方法。
详细位置
- 本页说明 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、缓存注解、自定义切面失效的根本原因:注解在方法上,但调用没有走到代理门口。
事务与 AOP
@Transactional 可以看作 Spring 提供的内置切面。它在方法调用前开启事务,在方法正常返回后提交事务,在异常符合回滚规则时回滚事务。
事务失效常见原因:
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 同类内部调用事务方法不生效 | this 调用绕过代理 | 把事务边界放到外部入口,或拆到另一个 Service |
| 异常发生但没有回滚 | 异常被捕获后吞掉 | 记录后继续抛出,或明确设置回滚规则 |
| 手动创建对象后事务不生效 | 对象不是 Spring Bean | 由容器注入对象 |
| 方法不可见或被限制 | 代理无法作为外部入口增强 | 把事务放在清晰的 public 应用服务方法上 |
事务边界通常放在应用服务层,而不是 Controller 或 Repository。Controller 太靠近协议,Repository 太靠近数据访问细节;Service 更适合表达“一次业务操作要保持一致”。
切点设计
切点设计的目标是“准”。过宽会误伤,过窄会漏掉。
常见方式:
| 方式 | 适合场景 | 风险 |
|---|---|---|
| 按注解匹配 | 审计、幂等、操作日志、业务开关 | 需要开发者显式标记 |
| 按包路径匹配 | 某一层统一治理,例如 Service 层日志 | 包结构变动会影响范围 |
| 按方法签名匹配 | 命名规范非常稳定的项目 | 命名变化可能误伤或漏掉 |
| 组合切点 | 需要精确限定范围 | 表达式复杂后可读性下降 |
优先使用注解表达业务意图。比如“这个方法需要审计”比“所有 save* 方法都审计”更稳定。
通知类型
| 通知 | 执行时机 | 适合做什么 |
|---|---|---|
| 前置通知 | 方法执行前 | 权限检查、参数快速校验 |
| 后置通知 | 方法结束后,无论成功失败 | 清理上下文 |
| 返回通知 | 方法正常返回后 | 成功审计、成功指标 |
| 异常通知 | 方法抛出异常后 | 异常统计、失败审计 |
| 环绕通知 | 包住整个调用 | 事务、耗时、限流、幂等 |
能用更窄通知时,不要默认使用环绕通知。环绕通知能力最大,也最容易隐藏调用行为。
工程场景
操作审计
审计记录“谁在什么时候做了什么”。它适合 AOP,因为审计不应混进每个业务方法的主流程。业务方法只标记需要审计,切面统一读取用户、动作、结果和异常。
需要注意:
- 审计失败是否影响业务成功,要有明确规则。
- 审计内容不要记录敏感明文,例如密码、完整 Token。
- 审计切点应尽量用注解,不要靠方法名猜测。
幂等控制
幂等控制用于防止重复提交。它适合 AOP 的前提是 Key 规则清晰、失败处理清晰、重复请求返回策略清晰。
需要注意:
- Key 应来自业务唯一信息,例如订单号、请求号、用户 ID 和动作组合。
- 占位或锁必须有过期时间。
- 方法失败后是否释放 Key,要按业务语义决定。
- 幂等不是简单加锁,它影响用户重复请求的最终语义。
日志与耗时
日志和耗时统计适合 AOP,但应该克制:
- 不要打印大对象和敏感字段。
- 不要在高频方法上做复杂序列化。
- 日志要包含可关联的请求 ID 或业务 ID。
- 耗时统计应能区分接口耗时、数据库耗时、外部调用耗时。
排查路径
注解不生效
按顺序检查:
- 目标类是否是 Spring Bean。
- 调用方拿到的是否是代理对象。
- 调用是否从外部进入,而不是
this内部调用。 - 切点是否能匹配方法。
- 方法可见性和类结构是否适合代理。
- 异常是否按预期继续传播。
切面执行顺序不对
多个切面同时命中时,应显式设置顺序。通常权限、限流、幂等这类“可能拒绝调用”的逻辑更靠前;事务包住需要一致性的业务操作;日志和指标负责记录结果。
性能异常
检查切点是否过宽,是否在高频方法中做了参数深拷贝、JSON 序列化、远程写日志等重操作。AOP 越靠底层、命中越频繁,越需要控制成本。
总结
AOP 的核心是代理边界。横切逻辑应该像方法外层的统一检查站,而不是隐藏业务流程的暗门。排查时先确认对象是否由容器管理,再确认调用是否经过代理,最后检查切点、顺序、异常传播和性能成本。
