谈谈 Spring 的 AOP?
结论先行:AOP(Aspect Oriented Programming,面向切面编程)把日志、事务、权限、缓存这类"横切关注点"从业务代码里抽出来,集中声明、按需织入。Spring AOP 是运行时基于动态代理的、方法级的、半自动 AOP:容器为被切中的 Bean 生成代理,外部调用先进入通知链(advice chain),再落到目标方法。它不修改字节码、不支持字段级连接点,这是与 AspectJ 最本质的差别。
一、概念对齐
| 概念 | 含义 | Spring 中的落地 |
|---|---|---|
| 切面 Aspect | 横切逻辑的模块化单元 | @Aspect 标注的类 |
| 连接点 JoinPoint | 程序执行中可以插入逻辑的点 | 只能是方法调用/执行 |
| 切点 Pointcut | 匹配连接点的表达式 | @Pointcut("execution(* com.demo.service.*.*(..))") |
| 通知 Advice | 在连接点处执行的动作 | @Before、@Around 等 |
| 织入 Weaving | 把切面接到目标上的过程 | 运行时生成代理(非编译期) |
| 目标 Target | 被代理的业务对象 | 原始 Bean |
二、调用链
外部调用 ──▶ 代理对象(Proxy)
│
├─ @Before 前置通知
├─ 目标方法执行(ProceedingJoinPoint.proceed)
├─ @AfterReturning / @AfterThrowing
└─ @After 最终通知
│
返回结果 / 抛出异常
通知执行顺序(同一连接点、Spring 5.3+ 统一语义):
| 通知 | 时机 | 能否改变返回值 | 能否吞异常 |
|---|---|---|---|
@Around | 包裹全程 | 能(可改返回值) | 能 |
@Before | 目标方法前 | 否 | 否 |
@AfterReturning | 正常返回后 | 能(returning 参数绑定,不建议改) | 否 |
@AfterThrowing | 抛异常后 | 否 | 能(不再抛即吞掉) |
@After | 无论成败(类似 finally) | 否 | 否 |
三、切点表达式与代码
@Aspect
@Component
@Order(1) // 多个切面时决定顺序,数字越小越外层
public class CostAspect {
// 切点:service 包下所有 public 方法,任意参数、任意返回值
@Pointcut("execution(public * com.demo.service..*.*(..))")
public void serviceMethods() {}
// 切点:所有带 @Loggable 注解的方法
@Pointcut("@annotation(com.demo.common.Loggable)")
public void annotated() {}
@Around("serviceMethods() || annotated()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed(); // 必须调用,否则业务不执行
} finally {
long cost = (System.nanoTime() - start) / 1_000_000;
System.out.printf("%s 耗时 %d ms%n", pjp.getSignature().toShortString(), cost);
}
}
}
// 注解式切面:业务侧只加注解,切面统一处理
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
String value() default "";
}
execution 表达式的结构:execution(修饰符? 返回值 包.类.方法(参数) throws?),其中 .. 表示"任意层级包/任意个参数",* 表示"任意",务必写对包名层级,否则切点悄悄不匹配。
四、Spring AOP 与 AspectJ
| 维度 | Spring AOP | AspectJ |
|---|---|---|
| 织入时机 | 运行时(创建 Bean 时生成代理) | 编译期/编译后/加载期 |
| 实现 | JDK 动态代理或 CGLIB | 修改字节码 |
| 连接点 | 仅方法执行 | 方法、构造器、字段、静态块等 |
| 是否需容器 | 需要(Bean 才被代理) | 不需要,普通 Java 对象也可 |
| 性能 | 每次调用多一层代理,可忽略 | 织入后接近原生调用 |
| 使用成本 | 零配置(starter 已启用) | 需要编译器/agent 配合 |
五、常见追问
- 问:AOP 与代理的关系? Spring AOP 的实现就是代理模式 + 拦截器链;没有代理就没有 AOP 效果,所以"自调用失效"实质是"没经过代理"。
- 问:
this和target的区别?this匹配代理对象类型,target匹配目标对象类型;JDK 代理下两者可能不同,写切点时用target更稳。 - 问:多个切面怎么排序?
@Order或实现Ordered;@Around的顺序是"外层先进入、后退出",像洋葱。 - 问:AOP 能拦 Controller 吗? 能,但 Controller 通常不被代理(没切点),且
@RequestMapping方法参数绑定发生在代理之内,切面拿不到原始 HttpServletRequest 之外的信息。
小结
回答"谈谈 AOP"分四层:是什么(横切关注点解耦)、怎么实现(动态代理 + 通知链)、怎么写(切面 + 切点表达式 + 通知类型)、边界在哪(仅方法级、需容器、自调用失效)。能把"代理是运行时织入的唯一手段"讲透,就已经超过背概念的水平。