✂️ AOP 原理
核心概念 · 代理选择策略 · 通知类型与执行顺序 · 自调用问题 · Spring AOP vs AspectJ
1. AOP 的核心概念?
AOP(面向切面编程):把"横切关注点"(日志、事务、权限、性能统计)从业务代码中抽取出来,在运行时织入。核心术语:
- 切面(Aspect):横切逻辑的模块化单元(@Aspect 类)
- 连接点(Join Point):能被织入的位置(Spring 中只能是方法调用)
- 切点(Pointcut):通过表达式匹配哪些连接点(execution(...)、@annotation(...))
- 通知(Advice):在切点处执行的逻辑,五种:
- @Before 前置(方法执行前)
- @After 后置(无论正常异常都执行,finally 语义)
- @AfterReturning 返回后(正常返回时,可拿到返回值)
- @AfterThrowing 异常后(拿到异常)
- @Around 环绕(最强大:控制方法执行、修改参数和返回值)
- 织入(Weaving):把切面代码应用到目标对象的过程。Spring 用运行时动态代理织入
@Around 环绕通知模板
@Aspect
@Component
class LogAspect {
@Around("execution(* com.demo.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed(); // 放行:执行目标方法
System.out.println("耗时 " + (System.currentTimeMillis() - start) + "ms");
return result;
} catch (Exception e) {
System.out.println("异常: " + e.getMessage());
throw e; // 异常要重新抛出
}
}
}
🎯 面试要点
- Spring AOP 只支持方法级别的连接点(拦截器链基于动态代理);AspectJ 原生还支持字段/构造器(编译期织入)
- 切点表达式常用:execution(修饰符 返回类型 类.方法(参数))、within(包)、@annotation(注解名)
- @Around 中
proceed()可以调用 0 次、1 次或多次(Spring 官方文档:"may be invoked once, many times, or not at all … All of these are legal")——多次调用会让目标方法执行多次;忘调则目标方法不执行
2. Spring AOP 用 JDK 代理还是 CGLIB?怎么决定?
- JDK 动态代理:目标实现接口 → 代理类 implements 接口,只能代理接口方法(详见 Java 基础页)
- CGLIB 代理:目标未实现接口 → 生成目标类的子类,重写方法;final 类/方法无法代理
- Spring Boot 2.x 起默认强制 CGLIB(即使有接口),因为:代理更完整(可代理非接口方法)、Spring 团队优化后性能相当、避免强制接口设计
代理创建的核心:AbstractAutoProxyCreator(BeanPostProcessor)在 Bean 初始化后检测是否有匹配切点 → 有则创建代理对象替换原 Bean。所以代理对象本身就是容器里的那个 Bean。
🎯 面试要点
- Spring AOP 是基于运行时代理的轻量 AOP;AspectJ 是编译期/加载期织入,更强但需要特殊编译器
- Spring 的 @Aspect 用的是 AspectJ 的注解语法,但执行机制仍是 Spring 自己的动态代理
- proxyTargetClass=true 强制 CGLIB;Spring Boot 默认即 CGLIB
3. 为什么 AOP/事务对"方法内部自调用"失效?(经典坑)
代理切面只对外部调用(经过代理对象)生效。类内部 this.method() 调用的是原始对象的方法,不走代理 → 切面不执行 → 事务注解失效。
失效与解决
@Service
class OrderService {
@Transactional
public void a() {
this.b(); // ❌ this 是原始对象,b() 的事务不生效
}
@Transactional
public void b() { ... }
}
// 解决方式:
// 1. 注入自身代理(Spring 构造器注入循环问题注意)
@Autowired private OrderService self; self.b();
// 2. AopContext.currentProxy()(需 @EnableAspectJAutoProxy(exposeProxy=true))
((OrderService) AopContext.currentProxy()).b();
// 3. 拆到另一个 Bean(最干净,推荐)
🎯 面试要点
- 所有基于代理的增强(事务、缓存 @Cacheable、异步 @Async、自定义切面)都有自调用问题
- 根治:把内层逻辑拆到独立 Bean 中调用
4. 多个切面/通知的执行顺序?
- 多个切面:按 @Order 值(越小越先)。@Order(1) 的 @Around 先进入、后退出(洋葱模型:外层环绕)
- 同一方法多种通知(Spring 5.2.7+ 语义):
- @Around 前 → @Before → 方法执行 → @AfterReturning/@AfterThrowing → @After → @Around 后
🎯 面试要点
- 旧版本(<5.2.7)After 可能在 AfterReturning 之前——新语义已统一,面试按新版答
- 事务切面一般 @Order 较低(内层),业务日志切面外层