Spring 的 AOP 在什么场景下会失效?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 代理原理理解:面试官不仅仅想知道你背了哪些失效场景,更想知道你是否理解 AOP 底层是基于动态代理实现的,代理对象和目标对象是两个不同的对象。

  2. 实战踩坑经验:考察你在实际项目中是否遇到过 AOP 失效的问题,能否快速定位原因。这块如果答不上来,面试官会觉得你实战经验不够。

  3. Spring 体系认知:AOP 失效场景涉及 Bean 生命周期、事务管理、注解使用等多个方面,能系统性地回答,说明你对 Spring 整体有深入理解。

核心答案

Spring AOP 失效的常见场景有 6 大类

序号 失效场景 核心原因 解决方案
1 同一个类内部方法调用 this.method() 走的是原始对象而非代理对象 注入自身 / AopContext.currentProxy()
2 方法非 public 修饰 Spring AOP 只能代理 public 方法 改为 public
3 方法被 final/static 修饰 无法被重写或被子类覆盖 去掉 final/static
4 目标对象未被 Spring 管理 不是 Spring Bean,无法生成代理对象 交给 Spring 容器管理
5 异常被 catch 吞掉 通知切不到异常,事务回滚失效 重新抛出或手动回滚
6 @EnableAspectJAutoProxy 未开启 没有激活 AOP 功能 加上该注解

下面逐个展开讲。

深度解析

一、内部方法调用(最常考、最常踩坑)

这是面试官最爱问的,也是实际开发中最容易踩的坑。

@Service
public class UserService {

    public void methodA() {
        // 直接用 this 调用,走的是原始对象,不是代理对象!
        this.methodB();
    }

    @Transactional
    public void methodB() {
        // 这里的事务注解会失效!
        db.save(user);
    }
}

为什么?来看一下调用链路:

上图展示了两种调用方式的本质区别:

  • 外部调用:经过代理对象,增强逻辑(切面、事务等)正常执行,AOP 生效。
  • 内部调用this.methodB() 中的 this 指向的是目标对象本身,而不是代理对象。等于直接跳过了代理层,增强逻辑根本没机会执行。

这个坑我在实际项目中踩过不止一次,尤其是在事务场景下——methodA()methodB(),结果 methodB() 上的 @Transactional 直接失效,数据写进去了一半报错,事务没回滚,搞得数据不一致。

解决方案有三种:

// 方案一:注入自身(推荐,最简洁)
@Service
public class UserService {
    @Autowired
    @Lazy  // 解决循环依赖
    private UserService self;

    public void methodA() {
        self.methodB(); // 通过代理对象调用
    }

    @Transactional
    public void methodB() {
        db.save(user);
    }
}

// 方案二:使用 AopContext(需要在启动类加 exposeProxy)
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {}

@Service
public class UserService {
    public void methodA() {
        ((UserService) AopContext.currentProxy()).methodB();
    }
}

// 方案三:拆分到不同的 Service(结构最清晰)
@Service
public class UserService {
    @Autowired
    private OrderService orderService;

    public void methodA() {
        orderService.methodB(); // 跨 Bean 调用,天然走代理
    }
}

二、方法修饰符问题

Spring AOP 有两种代理方式:

两种代理方式都 无法代理 privatestaticfinal 方法,原因很简单:

  • private 方法对外不可见,代理对象无法拦截
  • static 方法属于类级别,不参与多态
  • final 方法不能被子类重写,CGLIB 无法覆盖
@Service
public class UserService {

    @Transactional
    private void privateMethod() {
        // ❌ 事务失效,private 方法无法被代理
    }

    @Transactional
    public final void finalMethod() {
        // ❌ 事务失效,final 方法无法被 CGLIB 重写
    }

    @Transactional
    public static void staticMethod() {
        // ❌ 事务失效,static 方法不参与代理
    }
}

三、对象未被 Spring 容器管理

这个比较好理解。Spring AOP 的代理对象是在 Bean 初始化阶段生成的,如果你的类没有交给 Spring 管理(没有 @Component@Service 等注解,也不是通过 @Bean 注册的),那 Spring 根本不知道这个类的存在,自然不会为它生成代理。

// ❌ 没有 @Service 注解,不是 Spring Bean
public class UserService {
    @Transactional
    public void save() {
        // AOP 失效,Spring 不认识这个类
    }
}

四、异常处理不当

这个场景通常出现在 @Transactional 结合 AOP 的情况下:

@Service
public class UserService {

    @Transactional
    public void save() {
        try {
            db.save(user);
            int i = 1 / 0; // 抛出异常
        } catch (Exception e) {
            log.error("保存失败", e);
            // ❌ 异常被吞掉了,事务通知切不到,不会回滚
        }
    }
}

事务通知是通过捕获方法抛出的异常来触发回滚的。你把异常 catch 了不抛出去,事务切面以为一切正常,就直接提交了——数据就脏了。

五、其他边缘场景

还有几个不太常见但值得知道的:

  • 多线程场景:AOP 上下文是绑定在当前线程的(ThreadLocal),如果在方法内部开了新线程,新线程里拿不到代理上下文
  • 类加载器不一致:在不同类加载器下,代理类和目标类可能被认为不是同一个类型
  • Spring Boot 2.x 之后默认使用 CGLIB:如果之前依赖 JDK 动态代理的代码,升级后行为可能变化

面试高频追问

  1. JDK 动态代理和 CGLIB 的区别?哪个性能更好?

    • JDK 基于接口,CGLIB 基于继承。JDK 7 之前 CGLIB 快,JDK 8 之后 JDK 动态代理被大量优化,性能差距已经很小了。Spring Boot 2.x 默认走 CGLIB。
  2. @Transactional 失效和 AOP 失效是一回事吗?

    • 本质上是一回事。@Transactional 就是通过 AOP 实现的,AOP 失效的场景,事务注解也会失效。但事务还有自己特有的失效场景,比如异常类型不对(默认只回滚 RuntimeException,不回滚受检异常)。
  3. 如何在项目中排查 AOP 失效问题?

    • 第一,打个断点看调用栈,有没有经过代理类;第二,开启 Spring 的 debug 日志看代理创建过程;第三,直接 System.out.println(this.getClass()) 看输出的是不是代理类名。

常见面试变体

  • "Spring 事务在什么情况下会失效?"
  • "为什么同一个 Service 内部方法调用事务不生效?"
  • "JDK 动态代理和 CGLIB 有什么区别?Spring 默认用哪个?"
  • "@Transactional 注解加在 private 方法上会怎样?"

记忆口诀

"内非私静未开启"——内部调用、非 public、final、static、未托管、未开启。六个失效场景记这六个字就行。

总结

AOP 失效的根因就一个:没有经过代理对象。不管是内部调用绕过代理、方法修饰符导致无法代理、还是对象压根不受 Spring 管理,殊途同归。面试中重点把 "内部方法调用" 这个场景讲透,再顺带提一下其他几个场景,基本就能拿满分。