一、开篇引入
Spring框架的灵魂,一半藏在IoC容器里,另一半就刻在AOP的基因中。AOP(Aspect-Oriented Programming,面向切面编程)与IoC并称为Spring的两大内核,是每位Java开发者从“会写代码”走向“写出优雅代码”的必修课-13。

不少开发者在实践中都会遇到这样的困境:会用@Transactional加个事务,会写个日志切面,但一遇到AOP不生效就束手无策;面对面试官追问“JDK动态代理和CGLIB有什么区别”“内部方法自调用为什么不走代理”时,又答得支离破碎-21。
本文将带你由浅入深,从痛点切入,逐层拆解AOP的核心概念、底层原理与代码实战,并附上高频面试题的规范答案。全文贯穿问题→概念→示例→原理→考点的逻辑主线,力求让你看懂、记住、会用。

📌 本文为系列第一篇,后续将深入AOP源码与Spring事务底层原理,欢迎持续关注。
二、痛点切入:为什么要用AOP?
2.1 传统实现方式的问题
先看一个典型的业务场景:用户服务中需要记录每个方法的执行日志。
public class UserService { public void addUser(String name) { System.out.println("【日志】开始执行addUser方法"); // 核心业务逻辑:添加用户 System.out.println("【日志】addUser方法执行完毕"); } public void deleteUser(Long id) { System.out.println("【日志】开始执行deleteUser方法"); // 核心业务逻辑:删除用户 System.out.println("【日志】deleteUser方法执行完毕"); } }
这只是两个方法,日志代码已经占据了大量篇幅。如果项目有几十上百个Service,每个方法都要重复写同样的日志逻辑,代码膨胀到什么程度可想而知。
2.2 传统方式的三大痛点
| 痛点 | 具体表现 |
|---|---|
| 代码冗余 | 相同的横切逻辑在每个方法里重复编写,工作量成倍增长 |
| 耦合度高 | 日志、事务等非业务代码与核心业务代码纠缠在一起,修改一处可能影响多处 |
| 维护困难 | 日志格式或策略需要统一调整时,要逐个方法修改,极易遗漏 |
这些横跨多个模块的通用功能需求——日志记录、事务管理、权限校验、性能监控——在AOP的世界里被称为横切关注点(Cross-cutting Concerns) -39。AOP的设计初衷,正是将这些横切关注点从业务代码中“切”出来,封装成独立的模块,再以非侵入的方式“织入”到需要增强的方法中。
2.3 AOP带来的改变
使用AOP后,上面的代码可以简化为:
@Service public class UserService { public void addUser(String name) { // 只有核心业务逻辑,日志由AOP自动处理 } public void deleteUser(Long id) { // 只有核心业务逻辑 } }
日志逻辑被抽离到切面中统一管理,业务代码变得纯粹干净,横切逻辑也实现了复用。
三、AOP核心概念(概念A)
3.1 标准定义
AOP的全称是 Aspect-Oriented Programming,中文译为面向切面编程。它是一种编程范式,通过预编译方式和运行期动态代理实现程序功能的统一维护,允许在不修改源代码的情况下给程序动态添加扩展功能-31。
3.2 关键词拆解
切面(Aspect) :横切关注点的模块化封装。比如一个
LogAspect类,专门负责日志记录逻辑。连接点(Join Point) :程序执行过程中可以插入切面代码的位置。在Spring AOP中,连接点特指方法调用——即Spring容器中Bean的public方法执行点-31。
通知(Advice) :在特定连接点上执行的动作,也就是切面“做什么”。Spring AOP提供了五种通知类型-31:
| 通知类型 | 执行时机 |
|---|---|
| 前置通知(@Before) | 目标方法执行之前 |
| 后置通知(@After) | 目标方法执行之后(无论是否异常) |
| 返回通知(@AfterReturning) | 目标方法成功返回之后 |
| 异常通知(@AfterThrowing) | 目标方法抛出异常之后 |
| 环绕通知(@Around) | 包围整个方法调用,可控制是否执行原方法 |
切点(Pointcut) :通过表达式定义“哪些连接点需要被增强”,解决的是“在哪里做”的问题。一个切点可以匹配一个或多个连接点-31。
织入(Weaving) :将切面逻辑嵌入目标类的过程,这是AOP最核心的底层动作-21。
3.3 生活化类比
把程序想象成一栋大楼:
业务方法 = 各个楼层的办公室
切面 = 整栋大楼的中央安防系统
连接点 = 每个办公室的门口
切点 = 指定哪些办公室门口需要安防检查(比如财务室、机房)
通知 = 安防系统执行的动作(开门前验证身份、离开后锁门)
织入 = 把安防设备安装到指定办公室门口的过程
这样,每个办公室(业务方法)不需要自己操心安防逻辑,安防系统(切面)统一管理,既安全又整洁。
四、关联概念:Spring AOP vs AspectJ(概念B)
4.1 标准定义
AspectJ 是一个功能完整的Java AOP框架,支持编译时、类加载时和运行时三种织入方式,可以拦截构造函数、静态方法、字段访问等多种连接点-45。
4.2 二者的关系
很多初学者会混淆Spring AOP和AspectJ的关系。实际上,Spring AOP借用了AspectJ的注解风格(如@Aspect、@Before等),但底层实现完全不同。Spring AOP是基于动态代理的轻量级实现,而不是直接使用AspectJ的编译时织入能力。
4.3 核心差异对比
| 对比维度 | Spring AOP | AspectJ |
|---|---|---|
| 定位 | Spring框架自带的轻量级AOP实现 | 功能完整的独立AOP框架 |
| 织入时机 | 仅运行时织入(动态代理) | 编译时、类加载时、运行时 |
| 连接点范围 | 仅支持public方法执行 | 支持方法、构造器、字段访问等 |
| 拦截对象 | 仅Spring容器管理的Bean | 任意Java对象 |
| 性能 | 运行时代理,有一定开销 | 编译时织入,运行时无额外开销 |
| 依赖 | 无额外依赖(JDK原生或CGLIB) | 需要引入AspectJ编译器 |
4.4 一句话总结
Spring AOP是“够用就好”的轻量级运行时AOP,AspectJ是“功能完备”的全能型AOP框架——二者不是替代关系,而是互补关系。
在日常开发中,Spring AOP已足够覆盖绝大多数场景(事务、日志、缓存、权限等)。如果遇到需要对构造器、静态方法或非Spring管理的对象进行增强的需求,再考虑引入AspectJ。
五、代码实战:从零搭建一个日志切面
下面通过一个完整的示例,展示如何在Spring Boot项目中使用AOP。
5.1 引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
5.2 定义业务服务(目标对象)
@Service public class OrderService { public String createOrder(String productName, Integer quantity) { System.out.println("正在创建订单:" + productName + " x" + quantity); // 模拟业务逻辑 if (quantity == null || quantity <= 0) { throw new IllegalArgumentException("数量必须大于0"); } return "订单创建成功,订单号:ORD-" + System.currentTimeMillis(); } }
5.3 定义切面(横切逻辑)
@Aspect @Component public class LoggingAspect { // 定义切点:匹配OrderService中的所有方法 @Pointcut("execution( com.example.service.OrderService.(..))") public void servicePointcut() {} // 前置通知:方法执行前打印日志 @Before("servicePointcut()") public void logBefore(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); System.out.println("【前置通知】即将执行方法:" + methodName + ",参数:" + Arrays.toString(args)); } // 后置通知:方法执行后打印日志 @After("servicePointcut()") public void logAfter(JoinPoint joinPoint) { System.out.println("【后置通知】方法执行完毕:" + joinPoint.getSignature().getName()); } // 返回通知:方法成功返回后打印返回值 @AfterReturning(pointcut = "servicePointcut()", returning = "result") public void logAfterReturning(JoinPoint joinPoint, Object result) { System.out.println("【返回通知】方法返回:" + result); } // 异常通知:方法抛出异常时打印异常信息 @AfterThrowing(pointcut = "servicePointcut()", throwing = "error") public void logAfterThrowing(JoinPoint joinPoint, Throwable error) { System.out.println("【异常通知】方法异常:" + error.getMessage()); } // 环绕通知:完整控制方法执行(最常用) @Around("servicePointcut()") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); String methodName = joinPoint.getSignature().getName(); System.out.println("【环绕通知-前置】进入方法:" + methodName); try { Object result = joinPoint.proceed(); // 执行目标方法 long endTime = System.currentTimeMillis(); System.out.println("【环绕通知-后置】方法:" + methodName + ",耗时:" + (endTime - startTime) + "ms"); return result; } catch (Exception e) { System.out.println("【环绕通知-异常】方法:" + methodName + ",异常:" + e.getMessage()); throw e; } finally { System.out.println("【环绕通知-最终】方法:" + methodName + "执行结束"); } } }
5.4 执行流程说明
当调用orderService.createOrder("手机", 2)时,切面将按以下顺序执行:
环绕通知-前置 → 2. 前置通知 → 3. 执行目标方法 → 4. 返回通知(正常返回)或异常通知(抛出异常)→ 5. 后置通知 → 6. 环绕通知-后置/异常/最终
六、底层原理:动态代理
6.1 核心原理概览
Spring AOP的底层依赖动态代理技术。当Spring容器初始化一个Bean时,会检查这个Bean是否需要被代理(即是否匹配某个切点)。如果需要,Spring会动态生成一个代理对象,并将其注册到容器中替代原始Bean。当外界通过代理对象调用方法时,代理对象会在调用前后执行切面中定义的增强逻辑-39-22。
6.2 JDK动态代理 vs CGLIB
Spring AOP在底层使用两种动态代理技术-23:
| 对比项 | JDK动态代理 | CGLIB |
|---|---|---|
| 原理 | 基于接口,运行时生成实现接口的代理类 | 基于继承,运行时生成目标类的子类 |
| 是否需要接口 | 必须 | 不需要 |
| 能否代理final类/方法 | 不适用 | ❌ 不能 |
| 性能特点 | 调用成本较低 | 生成类成本较高,调用更快 |
| 依赖 | JDK原生,无额外依赖 | 依赖CGLIB库(spring-aop已包含) |
6.3 Spring的代理选择策略
Spring的默认策略非常简单-22:
if (目标类实现了至少一个接口) { 使用 JDK 动态代理 } else { 使用 CGLIB }
如果需要强制使用CGLIB,可以通过配置实现:
XML方式:
<aop:aspectj-autoproxy proxy-target-class="true"/>注解方式:
@EnableAspectJAutoProxy(proxyTargetClass = true)
6.4 AOP失效的常见场景(高频考点!)
| 失效场景 | 原因 | 解决方案 |
|---|---|---|
| 内部方法自调用 | 通过this调用不走代理对象 | 注入自身:@Autowired private XxxService self; |
| private/protected方法 | 代理类无法继承/重写非public方法 | 改为public方法 |
| final类/final方法 | CGLIB无法继承final类/重写final方法 | 去除final修饰符 |
| 非Spring容器管理的对象 | 切面仅对容器中的Bean生效 | 确保对象由Spring管理 |
| 切点表达式写错 | 匹配不到任何连接点 | 检查execution表达式语法 |
内部方法自调用的具体示例:
@Service public class OrderService { @Transactional // ⚠️ 这个事务会失效! public void createOrder(Order order) { // 业务逻辑 } public void batchCreate(List<Order> orders) { for (Order order : orders) { this.createOrder(order); // this调用不走代理,事务注解失效! } } }
正确写法:
@Service public class OrderService { @Autowired private OrderService self; // 注入自身代理对象 public void batchCreate(List<Order> orders) { for (Order order : orders) { self.createOrder(order); // 通过代理对象调用,事务生效 } } }
七、高频面试题与参考答案
面试题1:什么是AOP?Spring AOP的核心原理是什么?
参考答案:
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,旨在将横切关注点(如日志、事务、权限)从核心业务逻辑中分离出来,实现代码解耦和复用。Spring AOP的核心原理是动态代理——Spring容器在初始化Bean时,根据目标类是否实现接口,分别采用JDK动态代理(接口代理)或CGLIB(子类代理)生成代理对象,在代理对象的方法调用前后织入增强逻辑-6。
💡 踩分点:定义 + 核心原理(动态代理)+ 两种代理方式说明
面试题2:JDK动态代理和CGLIB有什么区别?Spring如何选择?
参考答案:
| 维度 | JDK动态代理 | CGLIB |
|---|---|---|
| 原理 | 基于接口,运行时生成实现接口的代理类 | 基于继承,运行时生成目标类的子类 |
| 接口要求 | 目标类必须实现至少一个接口 | 无接口要求 |
| 限制 | — | 无法代理final类/final方法 |
| 依赖 | JDK原生 | 需CGLIB库 |
Spring的默认选择策略:目标类实现了接口 → JDK动态代理;无接口 → CGLIB。可通过@EnableAspectJAutoProxy(proxyTargetClass = true)强制使用CGLIB-6-22。
💡 踩分点:三种差异 + 选择策略 + 配置方式
面试题3:Spring AOP和AspectJ有什么区别?
参考答案:
| 维度 | Spring AOP | AspectJ |
|---|---|---|
| 织入时机 | 仅运行时(动态代理) | 编译时、类加载时、运行时 |
| 连接点范围 | 仅public方法 | 方法、构造器、字段访问等 |
| 拦截对象 | 仅Spring容器管理的Bean | 任意Java对象 |
| 性能 | 运行时代理有一定开销 | 编译时织入,运行时无开销 |
日常开发中Spring AOP已足够使用;需要拦截构造器或静态方法时,可考虑引入AspectJ-45。
💡 踩分点:织入时机差异 + 连接点范围差异 + 适用场景建议
面试题4:为什么加了@Transactional注解,事务却不生效?
参考答案:
事务不生效的常见原因及解决方案:
内部方法自调用:通过
this调用同类中的@Transactional方法,不走代理对象 → 注入自身代理对象调用方法非public:Spring AOP默认只拦截public方法 → 改为public
异常被捕获:方法内部捕获异常未重新抛出 → 让异常抛出或手动回滚
异常类型不匹配:
@Transactional默认只回滚RuntimeException → 指定rollbackForfinal类/final方法:CGLIB无法代理 → 去除final修饰符-2-5
💡 踩分点:内部自调用(最核心)+ 非public方法 + 异常捕获 + 解决思路
面试题5:Spring AOP中的通知类型有哪些?
参考答案:
| 通知类型 | 注解 | 执行时机 |
|---|---|---|
| 前置通知 | @Before | 目标方法执行之前 |
| 后置通知 | @After | 目标方法之后(无论异常) |
| 返回通知 | @AfterReturning | 目标方法成功返回之后 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常之后 |
| 环绕通知 | @Around | 完全控制方法执行(最强大)-31 |
💡 踩分点:5种类型 + 执行时机(重点突出环绕通知)
八、结尾总结
核心知识点回顾
| 模块 | 核心要点 |
|---|---|
| AOP概念 | 横切关注点 + 切面 + 连接点 + 通知 + 切点 + 织入 |
| Spring AOP vs AspectJ | 运行时代理 vs 编译时织入;方法级 vs 全方位 |
| 代码实战 | @Aspect + @Component + @Pointcut + 5种通知 |
| 底层原理 | JDK动态代理(接口) vs CGLIB(继承) |
| 常见失效 | 内部自调用 + 非public + final + 非容器管理 |
进阶预告
本文从基础概念到代码实战,从底层原理到面试考点,完整梳理了Spring AOP的知识链路。下一篇我们将深入AOP的源码层面,剖析AnnotationAwareAspectJAutoProxyCreator的代理创建机制,以及Spring事务管理的底层实现原理。感兴趣的朋友可以持续关注。
📌 建议你将本文收藏或转发给需要的朋友,也欢迎在评论区交流讨论!