Spring AOP 失效的 8 种场景:切面加了没生效,每个原因都从源码说清楚

核心词汇: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 方法protectedprivate、包级私有方法,代理根本管不到。

解决

把方法改成 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 代理(有接口的情况下)。

解决

  1. 去掉 final 关键字
  2. 或者在切点表达式里排除这个类
  3. 如果必须用 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 调用不在它的管辖范围内。

解决

  1. 用 Gateway/Filter 层做统一拦截(所有请求都过网关)
  2. 用 Feign 的 RequestInterceptor 做请求层面的切面
  3. 如果是链路追踪,用 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 失效的场景,绝大多数都和**”调用有没有经过代理对象”**有关。记住这个核心逻辑,排查的时候就不会跑偏。

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注