Spring AOP
Spring AOP 通过代理,在 Bean 的方法调用前后执行事务、鉴权或监控逻辑。AOP 即面向切面编程,用于集中处理多个业务方法共有的逻辑。本文从连接点和切点入手,说明通知怎样组成拦截链,再分析顺序和代理入口对执行结果的影响。
1. 面试核心
先记住这几条,再读正文- 切点与增强。 Spring AOP 用 Pointcut 匹配目标方法,用 Advice 定义增强逻辑,再由 Advisor 组合两者。
- 拦截链。 拦截器调用
proceed()进入下一节点,目标方法结束后再按嵌套顺序逐层退出。卡 · 连接点与方法执行 - 切面顺序。
@Order数值越小,切面进入越早、退出越晚。卡 · 拦截器顺序 - 自调用边界。 代理模式下,目标对象的
this自调用绕过增强。卡 · 代理边界 - 退出通知。
@After覆盖正常和异常退出,@AfterReturning只覆盖正常返回。
2. 连接点与方法执行
连接点是可以插入增强逻辑的位置,Spring AOP 支持的是 Bean 的方法执行。下面用计时切面说明一次方法调用怎样经过增强:
@Aspect
@Component
class TimingAspect {
@Around("execution(* com.example.order.OrderService.*(..))")
Object measure(ProceedingJoinPoint point) throws Throwable {
long start = System.nanoTime();
try {
return point.proceed();
} finally {
System.out.println(System.nanoTime() - start);
}
}
}
示例用 execution 表达式匹配 OrderService 的方法。启用 AOP 后,外部调用先进入代理,再执行计时通知。通知调用 point.proceed() 继续执行业务,并在 finally 中输出耗时。因此,无论业务成功还是抛出异常,都会记录本次调用的时间。
字段访问和构造器连接点属于 AspectJ 织入的范围。织入是把增强逻辑加入字节码的过程,可以发生在编译或类加载阶段。
3. 常用概念
理解计时示例后,可以区分 AOP 中各个对象的职责。所谓横切规则,就是计时、鉴权这类跨越多个业务方法的共有逻辑。
| 概念 | 职责 |
|---|---|
| Aspect | 组织相关横切规则 |
| Pointcut | 匹配目标方法 |
| Advice | 执行增强逻辑 |
| Advisor | 组合 Pointcut 与 Advice |
| Proxy | 接收外部方法调用 |
| MethodInterceptor | 执行拦截链节点 |
代理按 Advisor 选择匹配当前方法的增强,再把它们组成拦截链。带运行时参数条件的切点,还会在调用时检查实际参数。因此,判断增强是否生效,需要确认三个环节:
- 调用是否经过代理入口。
- 切点是否匹配当前方法和参数。
- 拦截链是否包含预期的增强。
4. 拦截链
多个增强同时作用于一个方法时,会形成嵌套调用。每个拦截器收到当前方法调用的信息,执行前置逻辑后,通过 proceed() 进入下一节点。链走到底后调用目标方法,结果或异常再逐层返回,各层执行提交、回滚或记录逻辑。
监控 before
→ 事务 before
→ 目标方法
← 事务 commit / rollback
← 监控 record
环绕通知可以省略 proceed(),直接返回缓存等结果。这称为短路,此时后续拦截器和目标方法都会被跳过。反过来,多次调用 proceed() 会重复执行后续链和业务方法。所以,重试通知要限制次数,并确认重复执行是否会造成重复扣款等副作用。
5. 拦截器顺序
拦截器顺序决定哪个增强包住哪个增强,进而改变事务、重试和缓存的行为。不同切面用 @Order 或 Ordered 声明顺序。数值越小,优先级越高。高优先级切面进入较早,返回时较晚退出。
重试在事务外层时,每次尝试都重新进入事务拦截器。在无外层事务且传播为 REQUIRED 时,每次尝试创建新事务。重试位于事务内层时,多次尝试共享该事务。若数据库或内层事务已标记 rollback-only,也就是事务只能回滚,继续重试也无法让它正常提交。
事务 advisor 可通过 @EnableTransactionManagement(order=...) 排序。切面上的 @Order 用于该切面的 advice。相同优先级的不同切面应避免依赖偶然顺序。
关键组合要用集成测试记录实际顺序、事务状态和异常结果。缓存与事务的顺序还会影响提交前后的数据可见性,见缓存 key 与提交时机。多个自动代理功能可以参与同一代理的创建,注解数量与代理层数并非一一对应。
6. 切点范围
切点决定增强应用到哪些方法。表达式看起来相近时,先确认它检查的是声明类型、代理类型还是实际参数。
| 表达式 | 匹配依据 |
|---|---|
execution(* com.example.service..*(..)) | 方法签名与包范围 |
within(com.example.service..*) | 声明方法的类型范围 |
@annotation(com.example.Measured) | 方法注解 |
args(java.io.Serializable) | 实际参数的运行时类型 |
this(com.example.Service) | 代理对象类型 |
target(com.example.Service) | 目标对象类型 |
execution 看声明签名,args 看运行时实参。JDK 代理与目标类的类型不同,this 与 target 因此会有差异。
宽泛的包名切点容易同时拦截框架配置、健康检查和内部任务。业务范围明确时,可以用专用注解或稳定接口表达,再在启动或测试阶段核对实际匹配的方法。
高频 Advice 中的参数序列化和远程审计会增加请求耗时。增强逻辑也需要设置超时、处理异常,并对敏感参数脱敏。否则,共有的监控或审计逻辑会影响每一次业务调用。
7. 代理边界
本节按 Spring 的代理模式讨论注解为何没有产生预期效果。@Transactional、@Cacheable 和 @Async 都依赖调用经过对应代理。对象需交给代理基础设施处理,调用也需从代理入口进入。
7.1 同类自调用
同类自调用指目标对象用 this 调用自己的另一个方法。即使外部入口经过代理,内部这次调用也会直接到达目标方法。
public void createOrder() {
this.saveOrder();
}
@Transactional
public void saveOrder() {}
this.saveOrder() 在目标对象内部执行,绕过事务拦截器。若入口已有事务,内部方法仍在该事务内执行。只是内部注解声明的传播或回滚规则没有重新生效。
可将 saveOrder 移至另一个 Bean,显式通过其代理调用。也可以让外部入口承担完整的事务语义。自注入或 AopContext.currentProxy() 则会增加运行时约束,例如要求暴露当前代理。缓存和异步注解也受相同的自调用边界影响。
7.2 可代理的方法与类
可拦截范围由接口与继承规则决定,具体见 JDK 动态代理与 CGLIB。注解还要能被事务或缓存属性解析器找到,所以应使用实际部署版本验证业务入口。
7.3 异常与事务回滚
异常需到达事务拦截器并匹配回滚规则,具体见 Spring 事务。因此,捕获后只打印日志会让拦截器看到正常返回。转换异常时应保留 cause,也就是原始异常,并让新异常匹配回滚规则。
7.4 异步线程与事务上下文
异步调用需要同时考虑事务属于哪个线程,以及失败怎样通知调用方。JDBC 事务绑定当前线程,@Async 把方法交给执行器的另一个线程后,需要在该线程另建事务。异步方法通过代理进入时,异步与事务 Advisor 的组合决定具体执行边界。
调用方已经返回后,异步失败也无法沿原调用栈传播。返回 Future 或 CompletableFuture 的方法,可以通过结果对象承接失败。返回 void 时,则用 AsyncUncaughtExceptionHandler 处理未捕获异常。应用还可通过事件结果、重试和监控跟踪后续处理。
7.5 缓存 key 与提交时机
缓存增强需要先确定缓存项的身份,再确定结果何时可见。缓存 key 是定位缓存项的键,应包含租户和资源 ID 等影响业务结果的参数。设计 key 时,重点避免以下三类错误:
- 用可变参数作为键,写入后参数变化,使键与原结果失去对应关系。
- 自定义键使用不稳定的
toString(),导致同一业务对象在不同调用中得到不同键。 - 遗漏租户字段,让不同租户命中同一缓存项。
condition 控制是否应用缓存,unless 则可在方法执行后否决缓存写入。数据库提交前就写缓存,后续回滚会让缓存留下尚未生效的数据。可以在提交后失效或更新缓存,也可使用支持事务同步的缓存管理器延后变更。
数据库与远程缓存仍需分别处理失败,并明确缓存更新失败后的重试方式。最终时机要通过实际拦截链验证,因为切面顺序会改变执行位置。
7.6 多事务管理器
多个数据源或事务技术共存时,默认事务管理器未必管理当前访问的资源。可以通过 @Transactional(transactionManager="ordersTxManager") 显式指定,并在测试中核对实际绑定的连接。
本地事务只覆盖所属资源,多库原子性需要相应的全局事务支持,见 Spring 事务。
7.7 代理诊断
排查增强失效时,沿调用路径收集证据。重点检查以下五个环节:
- 用
AopUtils确认代理类型。 - 检查外部入口、自调用和切点匹配。
- 确认方法可见性、
final与注解位置。 - 开启事务、缓存或异步的诊断日志,记录拦截器顺序、线程名、事务状态和缓存 key。
- 对照实际 SQL,用集成测试复现成功、异常和并发路径。
8. 常见问题
8.1 @After 和 @AfterReturning 有什么区别?
@After 同时覆盖正常与异常退出,而 @AfterReturning 只在正常返回时执行。前者类似 finally,适合无论成功与否都要执行的清理。若只处理异常退出,可使用 @AfterThrowing。需要控制参数、返回结果或是否继续调用时,再使用 @Around。
8.2 AOP 能拦截手工 new 出来的对象吗?
普通 new 得到的是原始对象,直接调用会绕过 Spring 代理。如果确实需要增强手工对象,可以用 ProxyFactory 为它创建代理。之后调用方要使用代理返回的引用,才能进入拦截链。
9 · 面试题卡片
9.1 Spring AOP 的拦截链怎样执行
出现公司:美团
考察重点:解释代理怎样按切点选择拦截器,以及 proceed() 怎样形成嵌套调用。说明顺序为何会影响事务、重试和缓存。
参考回答
Spring AOP 通过代理选择匹配当前方法的拦截器,再按顺序嵌套执行。
容器先为需要增强的 Bean 创建代理,外部调用进入代理后,再按 Advisor 的 Pointcut 匹配。
每个拦截器执行前置逻辑后,用 proceed() 进入下一项。到达目标方法后,结果或异常逐层返回,各层再执行提交、回滚或记录逻辑。环绕通知也可以直接返回,或多次调用后续链,所以要控制短路与重复执行。
@Order 数值较小的切面进入较早、退出较晚。这会改变每次重试的事务范围,也会影响缓存结果的可见时机,应通过集成测试确认。
9.2 @Transactional 常见的失效原因有哪些
出现公司:美团、浩鲸科技
考察重点:根据代理入口、方法范围和异常传播,判断事务增强是否执行。说明线程切换与事务管理器选择怎样影响资源参与范围。
相关内容:代理边界。
参考回答
@Transactional 失效常见于绕过代理、方法不可拦截、回滚规则不匹配,以及线程或资源未加入事务。
- 调用要经过代理。 手工
new和this自调用绕过增强;自调用仍可沿用外层已有事务。 - 方法要可拦截。 JDK 代理接口需暴露方法,CGLIB 则无法覆盖
final、private方法。 - 异常要匹配回滚规则。 默认处理
RuntimeException与Error;受检异常可用rollbackFor指定,吞掉异常会影响代理判断。 - 线程和资源要正确参与。 原线程的 JDBC 事务不会自动进入异步线程,多数据源也需选对
transactionManager。
结合代理类型、线程、事务状态与 SQL 记录,再用异常路径测试验证回滚。
9.3 重试切面与事务切面的顺序怎样影响重试?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:根据切面的嵌套顺序,判断每次重试是重新开启事务,还是继续使用原事务。
相关内容:拦截器顺序。
参考回答
重试在事务外层时,每次尝试都会重新进入事务拦截器;重试在内层时,多次尝试共享同一事务。
无外层事务且使用 REQUIRED 时,各次尝试会创建新事务。
共享事务若已被标记为 rollback-only,后续重试也无法恢复正常提交。@Order 数值较小的切面位于外层,因此要明确排序,并用实际事务状态验证。
10. 参考资料
- Spring Framework:Spring AOP Capabilities and Goals
- Spring Framework:Advice API
- Spring Framework:Understanding AOP Proxies
- Spring Framework:Using
@Transactional - Spring Framework:Annotation Support for Scheduling and Asynchronous Execution
- Spring Framework:Cache Annotations
- Spring Framework:Declaring Advice
- Spring Framework:Declaring a Pointcut