核心词汇:AOP, 切面, 通知, 切点, 代理对象, 内部调用, CGLIB, JDK 动态代理, 织入
开篇:那些年,我们踩过的 AOP 坑
先讲个真实场景。
你写了一个性能监控切面,加了 @Around,想在每个 service 方法前后打时间戳。代码写好了,测试也通过了,上了生产——然后发现,有些方法的耗时根本没被记录。
你开始怀疑人生:切面明明生效了,为什么只有部分方法进了切面?
排查了半小时,最后发现一个让你想拍大腿的原因:那些”失效”的方法,全是同一个类里 a() 调用 b(),b() 加了切面,a() 没加,内部调用绕过了代理。
这种事,在 Spring 项目里太常见了。
AOP 失效的场景,总结下来就那么几种。但每一种踩到都是真金白银的线上事故。这篇文章,把 8 种最常见的失效场景全部拆解——不只是告诉你”怎么绕过”,更重要的是为什么失效,源码层面说清楚。
1. 先搞清楚:AOP 什么时候能生效
说失效之前,先说清楚 AOP 的工作原理。
Spring AOP 是基于代理实现的。容器给 Bean 生成一个代理对象,调用方拿到的是代理对象,而不是原始对象。切面的通知,都织入在这个代理对象里。
正常调用:
调用方 → 代理对象 → 原始对象 → 目标方法
同类内部调用:
原始对象 → 内部调用 → 目标方法(绕过代理!)
理解这个,就能理解为什么有些场景切面会失效:只要调用链不经过代理,切面就不会触发。
2. 场景一:同类内部调用(最常见)
类 A 的方法 methodA() 调用同类里的 methodB(),methodB() 上加了切面,但切面没进去。
@Service
public class OrderService {
public void createOrder(OrderDTO dto) {
// ① 调用同类里的私有方法
validate(dto); // ← 这里调用,跳过了代理
saveOrder(dto);
}
@Transactional // ② 切面加在这里
public void validate(OrderDTO dto) {
// 校验逻辑,@Transactional 理论上应该开启事务
// 但实际上事务根本没开启!
if (dto.getAmount() > 10000) {
throw new BizException("金额超限");
}
}
}
createOrder() 调用 validate(),是直接 this.validate(),不走代理对象,所以 @Transactional 切面不生效。
同一个对象的方法 A 调用方法 B,是直接通过 this 引用调用的,根本没经过 Spring 生成的代理对象。
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| ① 注入自身 | 把 Bean 注入到自身,从容器里拿代理对象 | 最简单,改动小 | 有循环注入风险 |
| ② ObjectProvider | 延迟注入,避免循环注入 | 无循环注入风险 | 调用时多一步 |
| ③ AopContext | 从 ThreadLocal 里拿当前代理对象 | 直接 | 暴露了实现细节 |
方案一:注入自身(最常用)
@Service
public class OrderService {
/**
* 把自身注入进来,注意不能用 @Autowired 直接注入(会循环)
* 解决:构造器注入,或者字段加 @Lazy
*/
@Autowired
@Lazy // 加 @Lazy 延迟注入,避免构造器循环
private OrderService self;
public void createOrder(OrderDTO dto) {
// 通过代理对象调用,走切面
self.validate(dto); // ← 走代理了!
saveOrder(dto);
}
@Transactional
public void validate(OrderDTO dto) {
if (dto.getAmount() > 10000) {
throw new BizException("金额超限");
}
}
}
方案二:ObjectProvider(更安全)
@Service
public class OrderService {
@Autowired
private ObjectProvider<OrderService> orderServiceProvider;
public void createOrder(OrderDTO dto) {
// 每次需要时从容器里拿
orderServiceProvider.getObject().validate(dto);
}
@Transactional
public void validate(OrderDTO dto) {
// ...
}
}
方案三:AopContext(不推荐,但要知道)
@EnableAspectJAutoProxy(exposeProxy = true) // 先在配置里开启 exposeProxy
public class AppConfig {}
// 然后在代码里手动拿代理对象
@Service
public class OrderService {
public void createOrder(OrderDTO dto) {
// 从 ThreadLocal 里拿当前代理对象
OrderService proxy = (OrderService) AopContext.currentProxy();
proxy.validate(dto);
}
@Transactional
public void validate(OrderDTO dto) {
// ...
}
}
推荐:优先用方案一(注入自身 + @Lazy),简单直接。方案三尽量别用,会让代码对 Spring AOP 产生强依赖。
3. 场景二:方法不是 public
切面拦截了某个方法,但断点怎么都进不去。
Spring AOP 默认只拦截 public 方法。protected、private、包级私有方法,代理根本管不到。
把方法改成 public,或者换一个切点表达式,只拦截你真正需要拦截的方法。
// private 方法加切面,没用,Spring AOP 管不了 private 方法
private void internalProcess() {
// ...
}
// 改成 public
public void internalProcess() {
// ...
}
4. 场景三:Bean 不在 Spring 容器里
自己 new 出来的对象,加了 @Autowired 或 @Transactional 都没用。
@Service
public class OrderService {
public void createOrder(OrderDTO dto) {
// 手动 new 的对象,不归 Spring 管
Validator validator = new Validator(); // ← 这个对象没有切面
validator.validate(dto); // 切面不生效
}
@Autowired
private Validator validator; // 这个才有切面
}
Spring AOP 只能给容器管理的 Bean 生成代理。自己 new 的对象,Spring 根本不知道,更不可能给它生成代理。
不要自己 new,从容器里拿 Bean。
@Service
public class OrderService {
@Autowired
private Validator validator; // 从容器里拿,有切面
public void createOrder(OrderDTO dto) {
validator.validate(dto); // ← 走代理了
}
}
5. 场景四:final 方法 / final 类
切面配置没问题,但 CGLIB 代理模式下,final 方法的切面不生效。
CGLIB 代理是通过继承实现的,生成的代理类是目标类的子类。final 方法不能被重写,所以切面织入不了。
public class BaseService {
public final void process() { // final 方法,不能被重写
// ...
}
}
JDK 动态代理没这个问题(JDK 代理是实现接口,不是继承),但 Spring 默认优先选 JDK 代理(有接口的情况下)。
-
去掉 final关键字 -
或者在切点表达式里排除这个类 -
如果必须用 final 类,改用 AspectJ 编译时织入(不是 Spring AOP)
// 方案二:在切点表达式里排除
@Around("execution(* com.example..*.*(..)) && !within(com.example.BaseService)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// ...
}
6. 场景五:切点表达式写错了
这是最冤的一种——切面没生效,不是因为原理没搞懂,而是切点表达式写错了。
错误一:包路径写错
// 想匹配 service 包,但写成了 servlet 包
"execution(* com.example.servlet.*.*(..))" // ← 匹配到错误的包
// 正确
"execution(* com.example.service.*.*(..))"
错误二:* 和 .. 分不清
// 想匹配 service 包及其所有子包,但写成了只匹配 service 下一层
"execution(* com.example.service.*.*(..))" // ← 只能匹配 service 下的类,不能匹配子包
// 正确:匹配 service 包及其所有子包
"execution(* com.example.service..*.*(..))" // 注意双点
错误三:方法名用 * 匹配不到具体方法
// 想匹配 saveUser 方法,但写错了方法名
"execution(* com.example.service.UserService.save(..))" // ← 名字对不上
// 正确
"execution(* com.example.service.UserService.saveUser(..))"
在切面里加一行日志,确认切点是否匹配到了:
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint joinPoint) {
// 每次进来都打印一下,看这个通知到底触发了没有
System.out.println("切面进来了:" + joinPoint.getSignature());
}
如果日志没打印,说明切点表达式没匹配上。用 --debug 启动 Spring Boot,看自动配置报告里的条件判断。
7. 场景六:同类上有多个切面注解
一个类同时加了 @Transactional 和自定义切面,结果一个生效、一个不生效。
Spring AOP 在处理同类上的多个切面时,会按 @Order 排序。但更重要的是:同类内部调用的问题依然存在(见场景一)。
如果一个方法 A 调用同类里的方法 B,方法 B 上的 @Transactional 不生效,即使方法 A 上也有 @Transactional——因为方法 A 调用 B 是走 this,不走代理。
参考场景一的解决方案,保证调用链经过代理对象。
8. 场景七:Spring Cloud/OpenFeign 远程调用
Feign 客户端接口上加了切面(如日志切面),但远程调用时切面没进去。
Feign 客户端的调用,是通过 HTTP 协议发出去的,不经过 Spring Bean 的代理对象。切面只能拦截 JVM 内部的调用,跨服务的 HTTP 调用不在它的管辖范围内。
-
用 Gateway/Filter 层做统一拦截(所有请求都过网关) -
用 Feign 的 RequestInterceptor做请求层面的切面 -
如果是链路追踪,用 SkyWalking / Jaeger 等 APM 工具
/**
* Feign 的请求拦截器,效果等同于切面
* 所有 Feign 远程调用都会经过这里
*/
@Configuration
public class FeignLogInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 在这里加日志、做鉴权、加 TraceId
template.header("X-Request-Id", UUID.randomUUID().toString());
}
}
9. 场景八:代理目标类选错了
有时候 Bean 有接口,但你想让切面拦截所有方法,包括从接口继承的方法。这种时候 Spring 可能选了 JDK 代理,但你想用 CGLIB。
Spring AOP 的代理选择策略:
Bean 有接口吗?
→ 有:优先用 JDK 动态代理
→ 没有:只能用 CGLIB
加了 @EnableAspectJAutoProxy(proxyTargetClass = true)?
→ 强制用 CGLIB,不管有没有接口
JDK 动态代理只能拦截接口里声明的方法,如果目标类自己扩充了接口之外的方法,JDK 代理管不到。
如果需要拦截所有方法,包括非接口方法,显式指定 CGLIB:
@EnableAspectJAutoProxy(proxyTargetClass = true) // 强制 CGLIB
public class AppConfig {}
// 或者在配置文件中
// spring.aop.proxy-target-class=true
10. 快速排查清单
遇到 AOP 疑似失效,按这个顺序查:
| 步骤 | 检查项 | 快速修复 |
|---|---|---|
| 1 | 切面类有没有 @Component? |
加上去 |
| 2 | 切面类有没有被组件扫描扫到? | 检查 @ComponentScan 范围 |
| 3 | 目标方法是不是 public? | 改成 public |
| 4 | 是不是同类内部调用? | 注入自身 + @Lazy |
| 5 | 是不是自己 new 出来的对象? |
从容器里拿 Bean |
| 6 | 切点表达式写对了吗? | 加日志打印确认 |
| 7 | 是不是 final 类 / final 方法? | 去掉 final 或换切点 |
| 8 | 需不需要强制 CGLIB? | 加 proxyTargetClass = true |
| 9 | 是不是远程调用(Feign/HTTP)? | 用网关或 RequestInterceptor |
11. 工具:如何确认一个对象是不是被代理了
Spring 提供了一个工具方法,可以快速检查一个对象是不是代理对象:
@Autowired
private ApplicationContext ctx;
public void checkProxy() {
Object bean = ctx.getBean("someBean");
// 判断是不是代理对象
System.out.println("is AopProxy: " + AopUtils.isAopProxy(bean)); // true/false
System.out.println("is JDK Proxy: " + AopUtils.isJdkDynamicProxy(bean)); // true/false
System.out.println("is CGLIB Proxy: " + AopUtils.isCglibProxy(bean)); // true/false
// 拿到原始对象
System.out.println("target class: " + AopUtils.getTargetClass(bean));
}
在排查的时候,先用这个方法确认 Bean 到底有没有被代理,可以快速缩小排查范围。
12. 面试题:AOP 失效的高频问题
Q1:同类内部调用为什么会导致 AOP 失效?
因为 Spring AOP 是基于代理实现的。同一个类里的方法 A 调用方法 B,是通过
this引用直接调用的,绕过了 Spring 生成的代理对象,所以切面不会触发。解决方法:把自身注入到 Bean 里,通过代理对象调用。
Q2:private 方法加切面为什么不生效?
Spring AOP 默认只拦截 public 方法,因为代理对象的方法访问权限和目标类一致,private 方法在子类中无法被重写,所以 CGLIB 代理无法拦截。
Q3:自己 new 的对象为什么没有切面?
Spring AOP 只能给容器管理的 Bean 生成代理。自己
new出来的对象不在 Spring 容器的管辖范围内,不会有代理。解决方法是从容器里拿 Bean,而不是手动 new。
Q4:final 类怎么加切面?
CGLIB 通过继承实现代理,final 类不能被继承,所以无法生成 CGLIB 代理。解决方法:去掉 final 关键字,或者改用 AspectJ 编译时织入(不依赖 Spring AOP)。
Q5:如何快速排查 AOP 失效?
用
AopUtils.isAopProxy()确认对象有没有被代理;用--debug模式查看条件评估报告;检查切点表达式是否有日志打印;按同类内部调用 → public 方法 → 容器管理 → 切点表达式这个顺序逐项排查。
小结
| 失效场景 | 根因 | 解决方法 |
|---|---|---|
| 同类内部调用 | this 引用绕过代理 |
注入自身 + @Lazy |
| 非 public 方法 | Spring AOP 只管 public | 改成 public |
| Bean 不在容器里 | 自己 new 的对象没有代理 | 从容器拿 Bean |
| final 方法/类 | CGLIB 继承不了 | 去掉 final 或换 AspectJ |
| 切点表达式写错 | 表达式匹配不上 | 加日志排查 |
| 远程调用(Feign) | HTTP 调用不经过代理 | 用网关或 RequestInterceptor |
| 多代理冲突 | 代理优先级问题 | 调整 @Order |
| 代理类型选错 | 有接口默认 JDK 代理 | 加 proxyTargetClass=true |
AOP 失效的场景,绝大多数都和**”调用有没有经过代理对象”**有关。记住这个核心逻辑,排查的时候就不会跑偏。