北京时间 2026年4月8日 发布 | 阅读时长约 8 分钟
在 Spring 框架的生态体系中,Spring AOP 与 IoC 并称为两大核心支柱。然而很多开发者在日常使用中只会添加 @Aspect 注解,一旦被问到“AOP 的底层是怎么实现的”“JDK 代理和 CGLIB 有什么区别”“为什么 @Transactional 有时候不生效”这类问题时,往往答不上来。本文将从问题出发,一步步拆解 Spring AOP 的核心概念、底层原理与实战代码,帮助你建立完整的知识链路,从容应对面试。

一、痛点切入:为什么需要 AOP?
先看一个典型场景:一个员工管理系统包含新增、删除、查询三个业务方法,现在需要为每个方法添加日志打印(记录入参、出参、执行时间)和权限校验。

如果不使用 AOP,传统实现是这样的:
@Service public class EmpService { private static final Logger logger = LoggerFactory.getLogger(EmpService.class); public void addEmp(Emp emp) { // 权限校验 if (!hasPermission("EMP_ADD")) { throw new RuntimeException("无权限"); } // 日志:记录入参和开始时间 long startTime = System.currentTimeMillis(); logger.info("addEmp方法入参:{}", emp); // 核心业务逻辑 System.out.println("新增员工:" + emp.getName()); // 日志:记录耗时 long endTime = System.currentTimeMillis(); logger.info("addEmp执行完成,耗时:{}ms", endTime - startTime); } public void deleteEmp(Long empId) { // 同样的权限校验、日志代码重复出现... } }
这种方式的问题很明显:代码冗余(相同的横切逻辑在多个方法中重复编写)、耦合度高(横切逻辑与业务逻辑紧密绑定)、维护困难(修改日志格式或权限规则需要改动所有业务方法)-8-52。
AOP(面向切面编程)正是为解决这类“横切关注点”问题而生——它允许开发者将日志、事务、安全等通用功能从业务代码中抽离,实现代码的解耦与复用-6。
二、核心概念:AOP 是什么?
AOP 全称 Aspect-Oriented Programming(面向切面编程),是一种通过预编译方式和运行期动态代理实现程序功能统一维护的技术。它的核心思想是将业务逻辑中的“横切关注点”(如日志记录、事务管理、权限验证等)与核心业务逻辑分离-8。
用一个生活化的类比来理解:想象一家餐厅,厨师负责“做菜”(核心业务)。如果每次做菜后都要让厨师自己“擦桌子、洗碗”(横切逻辑),厨师的工作效率会大幅下降。AOP 的做法是:雇佣一个专门的“服务员”负责这些横切事务。厨师只专注于做菜,服务员在厨师做菜前后自动完成擦桌子和洗碗。无论厨师做多少道菜,服务员的工作逻辑都只需维护一份,复用性极强。
三、关联概念:Spring AOP 核心术语详解
Spring AOP 涉及以下核心术语-6-8:
| 术语 | 英文 | 解释 |
|---|---|---|
| 切面 | Aspect | 横切关注点的模块化封装,如日志切面、事务切面,使用 @Aspect 注解标记 |
| 连接点 | Join Point | 程序执行过程中可插入增强逻辑的关键点,Spring AOP 仅支持方法执行级别 |
| 切点 | Pointcut | 匹配连接点的断言表达式,决定哪些连接点会被增强 |
| 通知 | Advice | 切面在特定连接点执行的具体动作,包括 5 种类型 |
| 织入 | Weaving | 将切面应用到目标对象并创建代理对象的过程,Spring 采用运行时动态织入 |
| 目标对象 | Target Object | 被一个或多个切面增强的业务对象 |
| AOP 代理 | AOP Proxy | Spring 创建的代理对象,用于实现增强逻辑 |
5 种通知类型的执行时机-6-11:
| 通知类型 | 注解 | 执行时机 |
|---|---|---|
| 前置通知 | @Before | 目标方法执行前 |
| 后置通知 | @AfterReturning | 目标方法正常返回后 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常后 |
| 最终通知 | @After | 目标方法执行后无论结果如何都执行 |
| 环绕通知 | @Around | 围绕目标方法执行,可完全控制方法执行时机,最强大 |
💡 环绕通知是重点:它相当于前置 + 后置 + 异常通知的集合,可以通过 pjp.proceed() 控制目标方法的执行,也是面试中经常被考察的通知类型-11。
四、概念关系梳理:AOP 与 OOP 的区别与联系
| 对比维度 | OOP(面向对象编程) | AOP(面向切面编程) |
|---|---|---|
| 核心单元 | 类(Class) | 切面(Aspect) |
| 解决什么问题 | 纵向功能划分(业务实体) | 横向横切关注点(日志、事务等) |
| 关注点 | 对象、继承、多态 | 方法增强、代理、织入 |
| 关系 | OOP 是主体 | AOP 是 OOP 的补充 |
一句话概括:OOP 负责纵向的业务模块划分,AOP 负责横向的通用功能抽取,二者相辅相成,共同构建高内聚低耦合的软件系统。
五、代码实战:Spring Boot 中使用 @Aspect 实现日志记录
1. 添加 Maven 依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
2. 编写业务类(目标对象)
@Service public class UserService { public String getUserInfo(Long id) { if (id <= 0) { throw new IllegalArgumentException("用户ID不能为负数"); } return "用户ID:" + id + ",姓名:张三"; } }
3. 编写切面类(核心)
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.; import org.springframework.stereotype.Component; @Aspect // ① 标记为切面类 @Component // ② 交给 Spring 管理 public class LogAspect { // ③ 定义切入点:匹配 com.example.service 包下所有类的所有方法 @Pointcut("execution( com.example.service..(..))") public void servicePointcut() {} // ④ 前置通知:记录方法调用信息 @Before("servicePointcut()") public void beforeMethod(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); System.out.println("【前置】调用方法:" + methodName + ",参数:" + Arrays.toString(args)); } // ⑤ 环绕通知:监控执行耗时(最常用、最强大) @Around("servicePointcut()") public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); System.out.println("【环绕-开始】" + pjp.getSignature()); Object result = pjp.proceed(); // 关键:执行目标方法 long cost = System.currentTimeMillis() - start; System.out.println("【环绕-结束】耗时:" + cost + "ms,返回:" + result); return result; } // ⑥ 异常通知:记录异常信息 @AfterThrowing(pointcut = "servicePointcut()", throwing = "ex") public void afterThrowing(JoinPoint joinPoint, Exception ex) { System.out.println("【异常】方法:" + joinPoint.getSignature().getName() + ",异常:" + ex.getMessage()); } }
⚠️ 关键点:@Aspect 标记的类会被 Spring 识别为切面,但 Spring 不会对该类本身进行动态代理,而是将其作为横切逻辑织入到目标对象中-1。
六、底层原理:Spring AOP 的实现机制
动态代理是 AOP 的底层基石
Spring AOP 本质上是用动态代理包装原始 Bean,让方法执行过程被增强-22。Spring 根据目标类的特性智能选择代理机制-24-8:
| 代理方式 | 原理 | 适用场景 | 依赖 |
|---|---|---|---|
| JDK 动态代理 | 基于接口,运行时生成实现目标接口的代理类 | 目标类实现了至少一个接口 | Java 标准库,无额外依赖 |
| CGLIB 代理 | 基于继承,运行时生成目标类的子类 | 目标类没有实现接口,或强制指定 | 需要引入 CGLIB 库 |
两种代理的核心实现流程-24:
JDK 动态代理:通过 Proxy.newProxyInstance() 创建代理对象,方法调用被转发到 InvocationHandler 的 invoke() 方法,再通过反射调用目标方法。
CGLIB 代理:通过 ASM 字节码技术生成目标类的子类,在子类中重写目标方法并插入增强逻辑,调用时直接走子类方法。
Spring 的默认策略
if (目标类实现了接口) { return JDK 动态代理; } else { return CGLIB 代理; }
📌 重要变化:Spring Framework 默认使用 JDK 动态代理;Spring Boot 2.x 开始将默认值改成了 CGLIB(通过 @EnableAspectJAutoProxy(proxyTargetClass = true))-42。
代理创建的核心入口
AnnotationAwareAspectJAutoProxyCreator 是整个 AOP 代理机制的核心入口。它是一个 BeanPostProcessor,会在 Bean 初始化之后(postProcessAfterInitialization 阶段)判断是否需要为该 Bean 创建代理对象,而不是在容器启动时提前创建-22。
七、高频面试题与参考答案
面试题 1:Spring AOP 是怎么实现的?JDK 动态代理和 CGLIB 有什么区别?
参考答案:
Spring AOP 的核心原理是动态代理。当目标对象实现了接口时,Spring 使用 JDK 动态代理,通过 Proxy.newProxyInstance() 生成代理对象;当目标对象没有实现接口时,Spring 使用 CGLIB 代理,通过生成目标类的子类来实现增强-30。
两者的核心区别:
| 区别维度 | JDK 动态代理 | CGLIB 代理 |
|---|---|---|
| 实现方式 | 基于接口 | 基于继承(生成子类) |
| 依赖 | Java 标准库,无需额外依赖 | 需要引入 CGLIB/ASM 字节码库 |
| final 类/方法 | ❌ 不适用 | ❌ 不可代理 |
| 性能 | 反射调用,性能略低 | 字节码直接调用,性能更高 |
| Spring 默认 | Spring Framework 默认使用 JDK | Spring Boot 2.x 默认使用 CGLIB |
加分回答:Spring 通过 DefaultAopProxyFactory 自动判断代理方式,也可以通过 @EnableAspectJAutoProxy(proxyTargetClass = true) 强制使用 CGLIB-8。
面试题 2:Spring AOP 和 AspectJ 有什么区别?
参考答案:
两者都是 Java 中实现 AOP 的框架,但定位完全不同-42-43:
| 对比维度 | Spring AOP | AspectJ |
|---|---|---|
| 实现方式 | 基于动态代理 | 基于字节码织入 |
| 织入时机 | 仅支持运行时织入 | 支持编译时、类加载时、运行时 |
| 功能范围 | 仅拦截 Spring 容器管理的Bean 方法 | 可拦截构造方法、静态方法、字段访问等 |
| 性能 | 略低(代理调用) | 更高(直接字节码织入) |
| 配置复杂度 | 低,与 Spring 生态集成度高 | 较高 |
| 使用定位 | 轻量级,适合大部分业务场景 | 功能全面,适合高级/复杂场景 |
一句话总结:Spring AOP 是轻量级的运行时代理方案,AspectJ 是全功能的编译/类加载时织入框架。
面试题 3:为什么 @Transactional 有时候不生效?有哪些常见陷阱?
参考答案(4 个常见陷阱):
非 public 方法:Spring AOP 默认只对 public 方法生效,private/protected 方法无法被代理拦截-31。
内部自调用:同一个类中的方法通过
this调用另一个被@Transactional注解的方法时,调用不会经过代理对象,导致事务失效-31。解决方案:通过ApplicationContext.getBean()获取代理对象再调用,或注入自身。异常类型不匹配:
@Transactional默认只对RuntimeException和Error回滚,checked 异常不会触发回滚,需要配置rollbackFor属性。代理方式限制:使用 JDK 动态代理时,目标类必须实现接口;使用 CGLIB 时,目标类不能是 final 类,目标方法不能是 final 方法。
加分回答:解决自调用问题的最佳实践是将事务方法抽取到单独的 Service 中,通过依赖注入调用,这样既清晰又符合单一职责原则。
八、结尾总结
本文围绕 Spring AOP 从 问题 → 概念 → 关系 → 实战 → 原理 → 面试 逐层展开,核心要点如下:
| 序号 | 核心要点 |
|---|---|
| ① | AOP 通过动态代理实现横切关注点与业务逻辑的解耦 |
| ② | 核心术语:切面(Aspect)、连接点、切点、通知(Advice)、织入 |
| ③ | 5 种通知类型:@Before、@AfterReturning、@AfterThrowing、@After、@Around |
| ④ | 两种代理方式:JDK 动态代理(基于接口)和 CGLIB 代理(基于继承) |
| ⑤ | 4 个常见陷阱:非 public 方法、内部自调用、异常类型、代理限制 |
💡 建议读者动手实践:在自己的 Spring Boot 项目中创建一个切面类,尝试拦截 Service 层方法,打印日志和执行时间。通过实际运行,你会更直观地理解代理对象的创建过程以及各通知类型的执行顺序。
下一篇文章我们将深入探讨 Spring AOP 的源码实现,从 @EnableAspectJAutoProxy 出发,逐步剖析代理创建的全流程。欢迎持续关注!
参考文献:
[1] AOP原理详解,2026-03-20-1
[2] Java外功精要——Spring AOP,2025-10-31-6
[3] Spring AOP 核心概念全解析,2025-07-29-8
[4] 基于注解的AOP,2026-02-08-11
[5] Spring Boot AOP 代理机制源码级深度解析,2026-01-20-22
[6] 百度后端开发面试题精选,2026-03-31-30
[7] Spring IOC与AOP核心概念,2026-03-11-31
[8] 面试鸭题库(Spring AOP 面试题)-42-43