在Java后端技术体系中,Spring AOP(面向切面编程) 与 IoC 并称为 Spring 框架的两大基石。如果 IoC 解决了对象之间的耦合问题,那么 AOP 就解决了“横切逻辑”的复用难题——比如日志记录、性能监控、权限校验、事务管理等,这些功能散落于业务代码各处,每处都要重复编写,维护起来极其繁琐-2。而 Spring AOP(Aspect Oriented Programming,面向切面编程) 的出现,正是为了用一种更优雅、更解耦的方式来应对这种“横切关注点”的代码治理。
很多开发者容易陷入“只会用却说不清”的困境:业务代码里加了 @Aspect 和 @Before 注解,AOP 确实跑起来了,但一问到“Spring AOP 底层是怎么实现的?JDK 动态代理和 CGLIB 有什么区别?为什么切面有时不生效?”就答不上来。本文将从痛点切入,由浅入深讲解 AOP 的核心概念、底层代理机制,提供可运行的代码示例,并整理高频面试考点,帮助你建立完整的知识链路,真正吃透 Spring AOP。

一、痛点切入:为什么需要 AOP?
在日常开发中,我们经常会遇到这样的场景:系统里的每一个业务方法都需要记录日志,或者都需要进行权限校验。如果按照传统的 OOP(Object-Oriented Programming,面向对象编程) 方式来实现,我们会怎么做?

// 传统方式:在业务代码中混合横切逻辑 @Service public class OrderService { public void createOrder(Order order) { // 日志记录(横切逻辑) System.out.println("开始创建订单,时间:" + System.currentTimeMillis()); // 权限校验(横切逻辑) if (!hasPermission()) throw new RuntimeException("无权限"); try { // 核心业务逻辑 System.out.println("执行订单创建核心业务"); } catch (Exception e) { // 异常处理(横切逻辑) System.out.println("发生异常:" + e.getMessage()); } // 日志记录(横切逻辑) System.out.println("订单创建完成"); } }
这种做法的缺点显而易见:
代码冗余严重:日志、权限、异常处理等代码在每个方法中重复出现;
耦合度高:业务逻辑与非业务逻辑混在一起,修改一处横切逻辑需要改动所有方法;
维护困难:若要调整日志格式,几十甚至上百个方法都得逐一修改;
违反单一职责原则:一个方法同时承担了业务逻辑和横切逻辑的职责。
OOP 擅长建模实体及其行为(比如 User 类有 login 方法),但像日志、事务、权限校验这类横跨多个类的逻辑,硬塞进每个方法里会破坏单一职责,也难复用-43。AOP 正是为了填补这一空白而诞生的。
AOP 的核心思想是:将横切关注点从业务逻辑中抽取出来,封装成独立的“切面”,在程序运行时动态地织入到目标方法中,实现无侵入式增强-2。
二、核心概念讲解:AOP 是什么?
什么是 AOP?
AOP(Aspect Oriented Programming,面向切面编程) ,是一种编程范式。它允许开发者在不改动业务代码的情况下,横向切入添加新的功能-1。AOP 以“切面(Aspect)”为单位对横切关注点进行模块化管理,可以减少系统重复代码,降低模块间的耦合度。
用一个生活例子来理解
想象一下你在电商平台下单:
核心关注点:创建订单、扣减库存、支付——这些是业务的主流程;
横切关注点:记录操作日志、检查用户是否登录、校验是否重复下单——这些是贯穿在所有操作前后的共性行为。
AOP 就像给这些核心操作加了一层“增强外壳”,在不修改业务代码的前提下,把日志、校验等功能统一注入进去。
AOP 的核心术语
| 术语 | 英文 | 解释 |
|---|---|---|
| 横切关注点 | Cross-cutting Concern | 需要被拦截、被增强的逻辑,如日志、事务 |
| 切面 | Aspect | 对横切关注点的抽象封装,相当于一个模块化的类 |
| 连接点 | Join Point | 程序执行中可被拦截的点(在 Spring 中即方法调用) |
| 切入点 | Pointcut | 筛选连接点的规则,定义“在何处”织入 |
| 通知 | Advice | 拦截到连接点后要执行的代码,定义“做什么” |
| 织入 | Weaving | 将切面应用到目标对象并生成代理对象的过程 |
一句话速记:切面(Aspect)= 切入点(Pointcut)+ 通知(Advice)
三、关联概念讲解:五种通知类型
通知(Advice)定义了切面在拦截到目标方法后具体要执行的逻辑。Spring AOP 提供了五种通知类型:
| 通知类型 | 注解 | 执行时机 |
|---|---|---|
| 前置通知 | @Before | 目标方法执行之前执行 |
| 后置通知 | @After | 目标方法执行之后执行(无论是否抛异常) |
| 返回通知 | @AfterReturning | 目标方法正常执行完毕并返回结果之后执行 |
| 异常通知 | @AfterThrowing | 目标方法执行过程中抛出异常时执行 |
| 环绕通知 | @Around | 包裹目标方法,可在执行前后均执行逻辑,还能控制方法是否执行 |
其中 环绕通知(@Around)功能最强,它是唯一能控制是否执行原方法、修改参数、替换返回值、捕获异常的增强类型。事务管理必须用 @Around,因为要在方法执行前开启事务、执行后提交或回滚;而单纯打日志,用 @Before 加 @AfterReturning 就够了-43。
⚠️ 注意:@Around 的参数必须是 ProceedingJoinPoint,且必须显式调用 proceed(),否则目标方法会完全不执行且无报错提示。
四、概念关系与区别总结
AOP 与 OOP 的关系,用一句话概括:OOP 负责“纵向”的对象建模,AOP 负责“横向”的切面织入。
| 对比维度 | OOP(面向对象编程) | AOP(面向切面编程) |
|---|---|---|
| 模块化单位 | 类(Class) | 切面(Aspect) |
| 关注点 | 实体及其行为 | 横切关注点(日志、事务等) |
| 解决什么问题 | 对象之间的职责划分 | 跨多个对象的通用逻辑复用 |
| 关系 | 主体编程范式 | OOP 的补充与完善 |
OOP 擅长建模实体及其行为,但像日志、事务、权限校验这类横跨多个类的逻辑,硬塞进每个方法里会破坏单一职责-43。AOP 就是将这类“横切关注点”抽出来,不侵入业务代码地织入执行流程,二者相辅相成。
五、代码示例:3 步实现一个方法耗时统计切面
下面通过一个实战案例——统计接口方法执行耗时,来快速体验 Spring AOP 的开发流程。
第一步:引入 AOP 依赖
Spring Boot 提供了 starter 依赖,直接在 pom.xml 中添加:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
第二步:编写切面类
import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; / 方法耗时统计切面 @Aspect:标识当前类是切面类 @Component:将切面类交给 Spring 管理 / @Aspect @Component @Slf4j public class TimeAspect { / @Around:环绕通知,包裹目标方法 切点表达式:匹配 com.example.demo.controller 包下所有类的所有方法 / @Around("execution( com.example.demo.controller..(..))") public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable { // ① 方法执行前:记录开始时间 long startTime = System.currentTimeMillis(); String className = joinPoint.getTarget().getClass().getSimpleName(); String methodName = joinPoint.getSignature().getName(); try { // ② 调用目标方法(核心业务逻辑) Object result = joinPoint.proceed(); // ③ 方法执行后:计算耗时并打印 long cost = System.currentTimeMillis() - startTime; log.info("【性能监控】{}.{} 执行耗时:{} ms", className, methodName, cost); return result; } catch (Exception e) { // ④ 发生异常时:记录异常信息 long cost = System.currentTimeMillis() - startTime; log.error("【性能监控】{}.{} 执行异常,耗时:{} ms,异常:{}", className, methodName, cost, e.getMessage()); throw e; } } }
第三步:使用并观察效果
@RestController public class UserController { @GetMapping("/user/{id}") public String getUser(@PathVariable Long id) { // 模拟业务处理耗时 try { Thread.sleep(100); } catch (InterruptedException e) {} return "User_" + id; } }
访问 /user/1,控制台将输出:
【性能监控】UserController.getUser 执行耗时:102 ms这段代码发生了什么? Spring 在运行时为 UserController 创建了一个代理对象。当外部调用 getUser 方法时,实际调用的是代理对象,代理对象先执行切面中的前置逻辑(记录开始时间),再调用原始目标方法,最后执行后置逻辑(计算耗时),整个过程对原始代码完全无侵入。
六、底层原理:Spring AOP 的动态代理机制
Spring AOP 的实现本质上依赖于代理模式。代理模式通过引入代理对象作为目标对象的中间层,实现了对目标对象访问的控制与增强-30。Spring AOP 底层采用动态代理技术,主要有两种实现方式:
JDK 动态代理
原理:基于 Java 反射机制。要求目标对象必须实现至少一个接口,然后通过
Proxy.newProxyInstance方法创建一个实现该接口的代理对象-32。过程:当调用代理对象的方法时,会触发
InvocationHandler的invoke方法,从而在方法调用前后插入增强逻辑。
CGLIB 动态代理
原理:基于字节码生成技术。当目标对象没有实现任何接口时,Spring AOP 会采用 CGLIB 来创建代理对象。CGLIB 通过继承目标类生成一个子类,并覆盖目标方法,在子类中插入增强逻辑-11。
限制:无法代理
final类或final方法,因为这些无法被继承或重写。
Spring / Spring Boot 中的代理选择策略
| 框架 | 默认代理策略 |
|---|---|
| Spring 框架 | 目标类有接口 → JDK 动态代理;无接口 → CGLIB |
| Spring Boot 2.0 之前 | 与 Spring 框架行为相同 |
| Spring Boot 2.0 及之后 | 默认使用 CGLIB 代理 |
在 Spring Boot 2.0 之后,即使目标类实现了接口,默认也会使用 CGLIB 代理-31。如需强制切换回 JDK 动态代理,可在配置文件中设置:
spring: aop: proxy-target-class: false
两种代理方式的性能对比
执行性能:CGLIB 创建的动态代理对象在实际运行时的性能比 JDK 动态代理高约 10 倍-;
创建性能:CGLIB 创建代理对象所花费的时间比 JDK 动态代理多约 8 倍-;
选择建议:对于单例 Bean 或实例池中的代理对象,由于无需重复创建,CGLIB 更合适;对于频繁创建的场景,JDK 动态代理更优。
核心理解:Spring AOP 底层依赖 Java 的反射机制(JDK 动态代理)和字节码操作(CGLIB),在运行时动态生成代理对象。正因如此,Spring AOP 只能拦截 public 方法——private、static、final 方法都无法被代理-43。同时,同一个类内部的方法调用(self-invocation)也不会触发 AOP 增强,因为调用走的是原对象而非代理对象-43。
七、高频面试题与参考答案
面试题 1:什么是 AOP?Spring AOP 的实现原理是什么?
标准答案:
AOP(Aspect Oriented Programming,面向切面编程)是一种编程范式,它通过横向抽取共性功能(如日志、事务)来解决代码重复问题。核心原理是动态代理,在目标方法前后织入增强逻辑-3。
Spring AOP 底层通过动态代理实现,主要有两种方式:
JDK 动态代理:基于接口实现,要求目标类必须实现接口;
CGLIB 代理:通过继承目标类生成子类代理,无需接口支持。
Spring 根据目标对象是否实现接口自动选择代理方式。在 Spring Boot 2.0 之后,默认使用 CGLIB 代理。
面试题 2:JDK 动态代理和 CGLIB 有什么区别?
| 对比维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 原理 | 基于反射,实现接口 | 基于字节码,继承父类 |
| 对目标类的要求 | 必须实现至少一个接口 | 无需接口,但不能是 final 类 |
| 可代理的方法 | 接口中定义的方法 | 非 final、非 static 的 public 方法 |
| 性能(执行) | 较低 | 较高(约快 10 倍) |
| 性能(创建) | 较快 | 较慢(约慢 8 倍) |
| 依赖 | JDK 原生支持 | 需要引入 CGLIB 库 |
面试题 3:为什么同⼀个类内部的方法调⽤ AOP 会失效?
标准答案:
AOP 的实现依赖于代理对象。当外部调用被代理对象的方法时,调用会经过代理对象,从而触发增强逻辑。但当一个类内部的方法直接调用另一个方法时(比如 this.method()),调用的是原始对象的方法,而不是代理对象的,因此切面不会生效。这就是所谓的 self-invocation 问题-43。
解决方案:
将方法拆分到不同的 Bean 中;
通过
AopContext.currentProxy()获取代理对象再调用;使用
@Autowired注入自身(注意循环依赖风险)。
面试题 4:Spring AOP 和 AspectJ 有什么区别?
| 对比维度 | Spring AOP | AspectJ |
|---|---|---|
| 实现方式 | 基于动态代理(运行时) | 基于字节码织入(编译时/类加载时/运行时) |
| 连接点范围 | 仅支持方法级别的连接点 | 支持字段、构造器、方法等多种连接点 |
| 性能 | 运行时有一定开销 | 编译期织入,运行时无额外开销 |
| 易用性 | 简单,与 Spring 集成良好 | 功能更强大,但配置相对复杂 |
面试题 5:介绍一下 AOP 的五种通知类型及其使用场景。
标准答案:
| 通知类型 | 使用场景 |
|---|---|
@Before | 前置校验、日志记录开始、权限检查 |
@After | 资源释放、清理操作(无论成功或失败都执行) |
@AfterReturning | 正常返回后的日志记录、结果加工 |
@AfterThrowing | 异常监控、错误上报、回滚事务 |
@Around | 事务管理、性能监控、缓存处理(功能最强) |
八、结尾总结
回顾本文的核心内容:
AOP 是什么:面向切面编程,用于将横切关注点(日志、事务、权限等)从业务逻辑中分离;
为什么需要 AOP:解决 OOP 无法有效处理横切逻辑的痛点,降低代码冗余和耦合度;
核心概念:切面(Aspect)= 切入点(Pointcut)+ 通知(Advice);
五种通知类型:
@Before、@After、@AfterReturning、@AfterThrowing、@Around;底层原理:JDK 动态代理(基于接口)和 CGLIB 代理(基于继承),Spring Boot 2.0+ 默认使用 CGLIB;
常见坑点:AOP 无法拦截
private/final方法,同一个类内部的 self-invocation 不会触发增强。
⚠️ 重点提醒:面试中常被问到“切面不生效”的问题,务必记住两个核心原因:一是目标方法不是 public 类型;二是发生了 self-invocation(类内部方法调用)。掌握了这两点,就能快速定位大多数 AOP 失效问题。
至此,你已经系统掌握了 Spring AOP 的核心知识体系。下一篇文章我们将深入 AOP 的切点表达式高级用法,以及如何结合自定义注解实现更优雅的切面配置,敬请期待。