谈谈 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 的核心思想,以及它和 OOP 的关系。

  2. 核心术语:是否准确理解切面、切点、通知、连接点等术语,能不能用实际代码举例说明。

  3. 实现原理:是否知道 Spring AOP 底层用的是动态代理(JDK 还是 CGLIB),以及在什么场景下用哪种代理。这个才是拉开差距的关键。

  4. 实战能力:能不能手写一个切面,知不知道常见的使用场景和踩坑点。

核心答案

AOP(Aspect-Oriented Programming,面向切面编程)的核心思想是:把横切关注点从业务逻辑中抽离出来,统一管理

啥叫横切关注点?日志、权限校验、事务管理、性能统计这些,几乎每个业务方法都需要,但又跟业务逻辑没半毛钱关系。如果每个方法都手写一遍,代码又臭又长。AOP 就是来解决这个问题的——"你只管写业务,这些脏活累活我来统一搞定"。

上图直观对比了有无 AOP 的代码组织方式。没有 AOP 时,每个业务方法都混入了日志、权限、事务这些非业务逻辑,代码重复且难以维护。有了 AOP 后,业务方法只关心自己的逻辑,横切关注点被抽取到切面中统一管理,代码清爽多了。

深度解析

一、核心术语

先把这些术语搞清楚,面试时才不会卡壳。

术语 英文 一句话解释
切面 Aspect 横切关注点的模块化,比如一个日志切面。就是一个包含通知和切点的类
连接点 Join Point 程序执行中的一个点,比如方法调用、异常抛出。Spring AOP 中特指方法的执行
切点 Pointcut 用来匹配连接点的表达式,决定通知要在哪些方法上生效
通知 Advice 在切点匹配到的方法上 "做什么事",比如打印日志、开启事务
织入 Weaving 把切面应用到目标对象的过程。Spring AOP 在运行时通过动态代理织入
目标对象 Target Object 被代理的原始对象
AOP 代理 AOP Proxy Spring 创建的代理对象,包裹了目标对象 + 切面逻辑

说人话:切点管 "在哪切",通知管 "切了干啥",切面就是把这两者打包在一起,织入就是把切面 "贴" 到目标对象上。

二、五种通知类型

Spring 提供了 5 种通知,对应方法执行的不同时机——

上图展示了五种通知在目标方法执行前后的触发顺序,关键理解点:

  • @Before:方法执行前触发,最简单最常用。适合做参数校验、权限检查。
  • @AfterReturning:方法正常返回后触发,可以拿到返回值。适合做结果日志、数据脱敏。
  • @AfterThrowing:方法抛出异常后触发,可以拿到异常对象。适合做异常告警、统一错误处理。
  • @After:方法执行后必定触发,不管有没有异常。类似 finally,适合做资源清理。
  • @Around:最强大的一种,包裹了整个方法执行过程,可以决定是否继续执行、修改参数、修改返回值。功能最全,但用不好也最容易出问题。

执行顺序:@Around 前半 → @Before → 目标方法 → @AfterReturning/@AfterThrowing@After@Around 后半

三、手写一个切面

光说不练假把式,来一个完整的日志切面:

@Aspect       // 声明这是一个切面
@Component    // 注册为 Spring Bean
public class LogAspect {

    // 定义切点:匹配 com.example.service 包下所有类的所有方法
    @Pointcut("execution(* com.example.service..*.*(..))")
    public void serviceLayer() {}

    // 前置通知
    @Before("serviceLayer()")
    public void before(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("调用方法:" + methodName + ",参数:" + Arrays.toString(args));
    }

    // 环绕通知(最强大,能控制是否继续执行)
    @Around("serviceLayer()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();

        // 可以修改参数:pjp.proceed(newArgs)
        Object result = pjp.proceed();  // 执行目标方法

        long cost = System.currentTimeMillis() - start;
        System.out.println(pjp.getSignature().getName() + " 耗时:" + cost + "ms");
        return result;  // 可以修改返回值
    }

    // 异常通知
    @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
    public void afterThrowing(Exception ex) {
        System.out.println("方法异常:" + ex.getMessage());
        // 发告警、记日志...
    }
}

切点表达式常用写法:

// 匹配 service 包下所有方法
execution(* com.example.service..*.*(..))

// 匹配带有 @Cacheable 注解的方法
@annotation(org.springframework.cache.annotation.Cacheable)

// 匹配 Service 层的所有 Bean(Bean 名称为 xxService)
bean(*Service)

// 组合使用:在 service 包下且带有 @Transactional 注解的方法
execution(* com.example.service..*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)

四、Spring AOP 的实现原理 — 动态代理

这是面试的 "灵魂拷问" 部分。Spring AOP 底层靠的是动态代理,有两种实现方式:

上图对比了两种代理方式的核心差异:

  • JDK 动态代理:利用反射在运行时动态生成一个实现了目标接口的代理类。Proxy.newProxyInstance() 创建代理对象,InvocationHandler 处理方法调用。限制是目标类必须实现至少一个接口,否则没法代理。Spring 默认情况下,如果目标类实现了接口,就用 JDK 动态代理。

  • CGLIB 代理:通过字节码技术在运行时生成目标类的子类,重写父类方法来实现代理。不需要接口,但不能代理 final 类和 final 方法(因为没法重写)。Spring 默认情况下,如果目标类没有实现接口,就用 CGLIB。

Spring Boot 2.x 之后有个重要变化:默认全部使用 CGLIB 代理spring.aop.proxy-target-class=true),不管你有没有接口。之前 Spring 默认是有接口用 JDK 代理、没接口用 CGLIB,这个行为差异面试的时候提一下很加分。

五、JDK 动态代理原理速览

// 1. 目标接口
public interface UserService {
    String getUser(Long id);
}

// 2. 目标实现
public class UserServiceImpl implements UserService {
    public String getUser(Long id) {
        return "用户" + id;
    }
}

// 3. 创建代理
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    (proxyObj, method, args) -> {
        // 前置逻辑(相当于 @Before)
        System.out.println("调用前:" + method.getName());

        // 执行目标方法
        Object result = method.invoke(target, args);

        // 后置逻辑(相当于 @AfterReturning)
        System.out.println("调用后,返回:" + result);
        return result;
    }
);

// 4. 调用代理方法
proxy.getUser(1L);  // 会先打印 "调用前",再执行方法,再打印 "调用后"

Spring AOP 在 BeanPostProcessor#postProcessAfterInitialization 阶段,检测到 Bean 被切面匹配,就会用上述方式创建代理对象,替换掉原始 Bean。

六、Spring AOP vs AspectJ

面试官偶尔会追问这个区别,简单了解就行——

维度 Spring AOP AspectJ
实现方式 运行时动态代理 编译时 / 加载时织入
功能范围 只能代理方法级别的连接点 支持方法、字段、构造器、静态方法等
性能 每次调用多一层代理,有损耗 编译期织入,运行时无额外开销
使用难度 简单,加注解就行 需要额外的编译器或 agent
适用场景 大多数业务场景 需要更细粒度控制的高级场景

日常开发 99% 用 Spring AOP 就够了。AspectJ 功能更强大但更复杂,除非你有特殊需求(比如需要拦截字段访问),否则没必要。

七、AOP 失效的常见场景

这个是生产中最容易踩的坑——同类内部方法调用,AOP 不生效

@Service
public class OrderService {

    public void createOrder() {
        // 直接调内部方法,AOP 不生效!
        this.validateOrder();
    }

    @Transactional  // 这个注解是通过 AOP 实现的
    public void validateOrder() {
        // 事务不会生效
    }
}

原因很简单:AOP 代理的是外部调用,this.validateOrder() 走的是原始对象的方法调用,不经过代理。解决方案:

  • validateOrder() 抽到另一个 Service 中
  • 通过 ApplicationContext 获取代理对象再调用
  • 使用 AopContext.currentProxy() 获取当前代理对象(需要开启 @EnableAspectJAutoProxy(exposeProxy = true)

面试高频追问

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

    JDK 基于接口,CGLIB 基于继承。早期 JDK 代理性能不如 CGLIB,但 JDK 8 之后大幅优化了反射性能,现在两者差距很小。Spring Boot 2.x 默认全部用 CGLIB,不用纠结这个。

  2. 追问二:Spring AOP 和 Spring 事务是什么关系?

    Spring 事务(@Transactional)本质上就是通过 AOP 实现的。Spring 创建一个事务切面(TransactionInterceptor),在方法执行前开启事务,正常返回时提交,异常时回滚。所以事务不生效的场景和 AOP 不生效的场景完全一致——内部调用、非 public 方法、final 方法等。

  3. 追问三:@Transactional 注解加在 private 方法上会生效吗?

    不会。因为 CGLIB 代理通过重写方法来实现拦截,private 方法无法被重写。JDK 代理基于接口,接口里也没有 private 方法。所以 @Transactional 只对 public 方法生效,这也是阿里开发手册里明确要求的。

常见面试变体

  • "说说 Spring AOP 的实现原理"
  • "JDK 动态代理和 CGLIB 有什么区别?"
  • "@Transactional 注解是怎么生效的?"
  • "AOP 在你项目中有哪些实际应用?"

记忆口诀

核心术语:切面打包,切点定位,通知干活,织入生效。

五种通知顺序:环绕前 → 前置 → 方法 → 返回/异常 → 后置 → 环绕后。

代理选择:有接口 JDK,没接口 CGLIB,Spring Boot 2.x 全用 CGLIB。

最大坑:内部调用不走代理,AOP 事务全失效。

总结

Spring AOP 的面试回答,核心就是三件事:概念讲清楚、代码写得出、原理说得透。概念上记住切面/切点/通知这几个术语,代码上能手写一个切面,原理上把 JDK 动态代理和 CGLIB 的区别讲明白。最后别忘了提一句 "内部调用 AOP 失效" 这个经典坑,面试官会觉得你有实战经验。