本文发布于北京时间 2026年4月10日,正值 Spring 框架在国内企业级开发中持续占据主导地位之际。在整理这篇文章时,我借助 华为AI文档助手 高效完成了资料检索与内容框架梳理。AOP(Aspect Oriented Programming,面向切面编程)是 Spring 框架的两大核心思想之一(另一个是 IoC 控制反转),也是 Java 后端面试中出现频率高达 85% 以上的必考点-28。然而很多学习者面临一个普遍困境:会使用 @Aspect 注解写切面,但说不清 AOP 的底层原理;知道 JDK 动态代理和 CGLIB,却在面试时答不出核心区别。本文将带你由浅入深,一次性彻底搞懂 Spring AOP。
一、痛点切入:为什么需要 AOP?

先来看一个典型的业务场景——用户登录、下单、支付、查询,每个方法都需要日志记录、权限校验、事务控制、性能监控。如果按传统方式在每个方法里手动写一遍:
// ❌ 传统方式:每个方法都要写重复代码@PostMapping("/delete") public BaseResponse deleteApp(long id) { User user = checkPermission(); // 重复代码1 if (!user.isAdmin()) { // 重复代码2 throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 真正的业务逻辑... log.info("deleteApp执行完成"); // 重复代码3 } @PostMapping("/update") public BaseResponse updateApp(App app) { User user = checkPermission(); // 重复代码1 if (!user.isAdmin()) { // 重复代码2 throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 真正的业务逻辑... log.info("updateApp执行完成"); // 重复代码3 }
这种传统实现方式存在三大致命缺陷:
耦合度高:横切逻辑(日志、权限)与业务逻辑强行捆绑
代码冗余:相似代码在多个方法中重复出现,重复率可达 60% 以上-36
维护困难:改一个权限规则,需要修改所有业务方法
AOP 正是为解决这些问题而生——将横切关注点(Cross-cutting Concerns)抽离成独立的“切面”,在不修改原有业务代码的前提下,对方法进行增强,统一处理日志、事务、权限、监控等横切逻辑-18。
// ✅ 有AOP后:业务代码只关心业务本身 @PostMapping("/delete") @AuthCheck(mustRole = "admin") // 仅需一个注解 public BaseResponse deleteApp(long id) { // 只有真正的业务逻辑... } @PostMapping("/update") @AuthCheck(mustRole = "admin") // 同样的注解 public BaseResponse updateApp(App app) { // 只有真正的业务逻辑... }
一句话总结痛点:AOP 让业务开发者只关心“做什么”,不再为“顺便做什么”而烦恼-17。
二、核心概念讲解:切面(Aspect)
标准定义:Aspect(切面)——将横切关注点进行模块化的类,用 @Aspect 注解标注-17。
拆解关键词:
“横切关注点”:那些横向贯穿多个业务模块的功能,如日志、事务、权限
“模块化”:把这些功能从业务代码中抽出来,封装成独立的类
生活化类比——小区门禁系统:
小区 = 整个应用程序
每家每户 = 各个业务类
大门 = 方法入口
保安 = 切面(Aspect)
检查规则 = 切入点表达式
检查流程 = 通知-17
核心作用:将公共功能从业务代码中“抽离”出来,实现业务逻辑与非业务逻辑的彻底解耦。
@Aspect // 声明这是一个切面类 @Component // 交给Spring容器管理 public class AuthInterceptor { // 整个类封装了权限校验的所有逻辑 }
三、关联概念讲解:通知(Advice)
标准定义:Advice(通知)——切面在特定切入点执行的具体增强逻辑,用 @Before、@After、@Around 等注解标注-18。
切面与通知的关系:
| 概念 | 角色 | 类比 |
|---|---|---|
| 切面(Aspect) | 容器/模块 | 保安这个人 |
| 通知(Advice) | 具体动作 | 检查证件、登记信息、联系业主 |
五种通知类型:
| 通知类型 | 注解 | 执行时机 | 典型场景 |
|---|---|---|---|
| 前置通知 | @Before | 目标方法执行前 | 日志记录、参数校验 |
| 后置返回通知 | @AfterReturning | 目标方法正常返回后 | 返回值处理、缓存更新 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常时 | 异常告警、事务回滚 |
| 最终通知 | @After | 方法执行完成后(无论成败) | 资源释放、清理工作 |
| 环绕通知 | @Around | 完全控制方法执行前后 | 权限校验、性能监控-20 |
执行顺序示例:
正常执行流程:
@Around前置处理 → @Before前置通知 → 执行原方法 → @AfterReturning后置返回通知 → @After最终通知 → @Around后置处理
异常执行流程:
@Around前置处理 → @Before前置通知 → 执行原方法(抛出异常) → @AfterThrowing异常通知 → @After最终通知-20
⚠️ 易错点:@Around 需要手动调用 proceed() 让原始方法执行,且返回值必须声明为 Object 类型,否则会吞掉返回值-18。
四、概念关系与区别总结
一句话记忆:切面是“容器”,通知是“动作”;切面告诉你“有什么功能”,通知决定“何时做”。
| 对比维度 | 切面(Aspect) | 通知(Advice) |
|---|---|---|
| 本质 | 模块化的类 | 类中的方法 |
| 标注 | @Aspect | @Before/@After/@Around 等 |
| 作用 | 定义有哪些增强功能 | 定义增强逻辑何时、如何执行 |
| 类比 | 工具箱 | 工具箱里的工具 |
五、完整代码示例
场景:实现一个方法执行耗时监控的切面。
步骤1:添加依赖
<!-- Spring Boot AOP 起步依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
步骤2:编写切面类
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Aspect // ① 声明切面 @Component // ② 交给Spring管理 @Slf4j public class PerformanceAspect { // ③ 定义切点:匹配 service 包下所有类的所有方法 @Pointcut("execution( com.example.service..(..))") public void serviceMethods() {} // ④ 环绕通知:计算执行耗时 @Around("serviceMethods()") public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); // 执行原始方法 Object result = joinPoint.proceed(); long endTime = System.currentTimeMillis(); log.info("{} 执行耗时: {}ms", joinPoint.getSignature().getName(), (endTime - startTime)); return result; } }
步骤3:业务代码(无需任何修改)
@Service public class UserServiceImpl implements UserService { @Override public User getUserById(Long id) { // 只关心业务逻辑,AOP会自动织入性能监控 return userMapper.selectById(id); } }
执行流程解读:
Spring 启动时扫描到
@Aspect注解根据
@Pointcut表达式找到需要增强的目标方法运行时创建代理对象,当调用
getUserById()时,实际调用的是代理对象的方法代理对象先执行环绕通知的前置逻辑(记录开始时间)
通过
joinPoint.proceed()调用原始业务方法执行环绕通知的后置逻辑(计算耗时并打印日志)
六、底层原理:动态代理
Spring AOP 底层依赖的核心技术是 动态代理,通过代理对象在运行时动态地将切面逻辑织入目标方法-37。
6.1 两种代理方式
| 对比维度 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|
| 实现原理 | 基于 Java 反射机制,通过 Proxy 类和 InvocationHandler 接口实现 | 通过继承目标类生成子类,覆盖方法实现增强 |
| 目标类要求 | 必须实现接口 | 无需实现接口(但不能是 final 类) |
| 代理生成 | 生成实现接口的代理对象 | 生成目标类的子类对象 |
| 性能 | 反射调用,性能相对较低 | 直接调用,性能通常更高 |
| 依赖 | JDK 原生,无需额外依赖 | 需要引入 CGLIB 库(Spring 已集成) |
| 限制 | 只能代理接口中定义的方法 | 无法代理 final 类和 final 方法-29 |
6.2 Spring 的代理选择策略
Spring 默认使用 JDK 动态代理,当目标类没有实现任何接口时,自动切换到 CGLIB。如需强制使用 CGLIB,可通过以下配置:
@Configuration @EnableAspectJAutoProxy(proxyTargetClass = true) // 强制使用 CGLIB public class AopConfig {}
底层依赖铺垫:AOP 的上层实现依赖于 Java 反射机制、代理模式设计思想以及 Spring IoC 容器的 Bean 管理能力。后续进阶内容我们将深入 ProxyFactory 的源码实现。
七、高频面试题与参考答案
面试题1:什么是 Spring AOP?它解决了什么问题?
踩分点:定义 + 横切关注点 + 与 IoC 的关系 + 实现原理关键词
参考答案:
AOP(Aspect Oriented Programming,面向切面编程)是 Spring 的两大核心思想之一(另一个是 IoC)。它通过将横切关注点(如日志、事务、权限、监控等非业务逻辑)从业务代码中横向抽取出来,模块化为独立的切面,在不修改原有业务代码的前提下,对方法进行增强。AOP 的底层实现依赖动态代理技术(JDK 动态代理或 CGLIB)-18。
面试题2:Spring AOP 的底层实现原理是什么?JDK 动态代理和 CGLIB 有什么区别?
踩分点:动态代理 + 两种方式的实现原理 + 核心区别 + Spring 的选择策略
参考答案:
Spring AOP 底层基于动态代理实现,在运行时动态生成代理对象,将切面逻辑织入目标方法。
JDK 动态代理:基于 Java 反射机制,通过 java.lang.reflect.Proxy 类和 InvocationHandler 接口实现,要求目标类必须实现接口,生成的代理对象实现了相同的接口。
CGLIB 动态代理:通过继承目标类生成子类,重写父类方法实现增强,无需接口,但无法代理 final 类或 final 方法。
Spring 的选择策略:默认使用 JDK 动态代理;当目标类未实现任何接口时,自动切换到 CGLIB;可通过 @EnableAspectJAutoProxy(proxyTargetClass = true) 强制使用 CGLIB-29-37。
面试题3:AOP 中的 Join Point 和 Pointcut 有什么区别?
踩分点:概念区分 + 类比 + 举例
参考答案:
Join Point(连接点) :所有可以被拦截的方法执行点,是“候选者”集合。例如 Controller 或 Service 层中的所有方法。
Pointcut(切入点) :从连接点中筛选出来真正需要增强的那部分方法,是“被选中者”。通过切入点表达式(如
execution(...)或@annotation(...))来定义筛选规则。
一句话区分:Join Point 是“所有门”,Pointcut 是“需要保安检查的门”-17。
面试题4:五种通知类型的执行顺序是怎样的?
踩分点:正常流程 + 异常流程 + 各自特点
参考答案:
正常执行:@Around前置 → @Before → 目标方法 → @AfterReturning → @After → @Around后置
异常执行:@Around前置 → @Before → 目标方法(抛异常) → @AfterThrowing → @After
关键差异:
@AfterReturning只在成功时执行@AfterThrowing只在异常时执行@After类似finally,无论如何都会执行@Around功能最强大,但需要手动调用proceed()-20
面试题5:为什么 Spring AOP 不能拦截 private 方法?
踩分点:代理机制原理
参考答案:
因为 Spring AOP 基于动态代理实现。JDK 动态代理只能代理接口中定义的 public 方法;CGLIB 通过继承生成子类,无法重写父类的 private 方法。因此只有 public 方法可以被 AOP 拦截增强。如果需要拦截 private 方法,可以考虑使用 AspectJ 的编译时织入(LTW 加载时织入)方式。
八、结尾总结
核心知识点回顾
| 概念 | 核心要点 |
|---|---|
| AOP | 面向切面编程,抽离横切关注点 |
| 切面(Aspect) | 用 @Aspect 标注的模块化类 |
| 通知(Advice) | @Before/@After/@Around 等五种类型 |
| 连接点(Join Point) | 所有可被拦截的方法(候选) |
| 切入点(Pointcut) | 真正需要增强的方法(选中) |
| 底层原理 | JDK 动态代理 + CGLIB |
| 与 IoC 关系 | AOP 依赖 IoC 容器管理 Bean |
重点与易错点提醒
Join Point 是全集,Pointcut 是子集——不要混淆概念
@Around必须调用proceed()——否则原方法不会执行,返回值必须是ObjectJDK 代理要求实现接口,CGLIB 不能代理 final 类——面试必考对比
private 方法无法被 AOP 拦截——受代理机制限制
预告:下一篇将深入剖析 Spring AOP 的源码实现,从 ProxyFactory 到 JdkDynamicAopProxy,带你彻底读懂 AOP 的“骨架”是如何搭建的。如果你对 Spring IoC 的循环依赖解决机制感兴趣,也欢迎在评论区留言,我们下期再会!
