跳到主要内容
约 9 分钟3 张卡

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 代理诊断​

排查增强失效时,沿调用路径收集证据。重点检查以下五个环节:

  1. 用 AopUtils 确认代理类型。
  2. 检查外部入口、自调用和切点匹配。
  3. 确认方法可见性、final 与注解位置。
  4. 开启事务、缓存或异步的诊断日志,记录拦截器顺序、线程名、事务状态和缓存 key。
  5. 对照实际 SQL,用集成测试复现成功、异常和并发路径。

8. 常见问题​

8.1 @After 和 @AfterReturning 有什么区别?​

@After 同时覆盖正常与异常退出,而 @AfterReturning 只在正常返回时执行。前者类似 finally,适合无论成功与否都要执行的清理。若只处理异常退出,可使用 @AfterThrowing。需要控制参数、返回结果或是否继续调用时,再使用 @Around。

8.2 AOP 能拦截手工 new 出来的对象吗?​

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. 参考资料​