面向切面编程(AOP)是Spring框架的三大核心特性之一,与IoC并驾齐驱。本文将由浅入深,从痛点切入到源码原理,配合代码示例与高频面试题,帮助读者彻底掌握Spring AOP。
一、开篇引入:AOP——Spring框架中不容忽视的核心技术

Spring框架有两大基石:IoC(Inversion of Control,控制反转)和AOP(Aspect-Oriented Programming,面向切面编程)。IoC解决对象之间的依赖管理,AOP则解决横切关注点(Cross-Cutting Concerns)的模块化问题。
很多开发者在实际工作中虽然天天用AOP,却常常陷入这样的困境:会用@Before、@After注解,却说不出AOP到底是什么原理;能写出切面代码,却讲不清切点和连接点的区别;面试被问到动态代理时支支吾吾,只能含糊带过。

本文将围绕AOP的痛点引入→核心概念→与OOP的关系→代码实战→底层原理→面试考点这条主线,系统讲解Spring AOP,帮助读者建立完整知识链路。
二、痛点切入:为什么需要AOP?
2.1 传统实现方式的问题
假设我们要在一个业务系统中添加日志记录功能。没有AOP时,代码通常是这样的:
public class UserService { public void saveUser(User user) { System.out.println("【日志】调用saveUser方法,参数:" + user); // 核心业务逻辑 System.out.println("【日志】saveUser方法执行完毕"); } public void deleteUser(Long id) { System.out.println("【日志】调用deleteUser方法,参数:" + id); // 核心业务逻辑 System.out.println("【日志】deleteUser方法执行完毕"); } // 每个方法都要重复写日志代码... }
2.2 传统方式的四大痛点
以上代码暴露了明显的缺陷:
| 痛点 | 具体表现 |
|---|---|
| 代码冗余 | 日志代码在每个方法中重复出现 |
| 耦合度高 | 日志逻辑与业务逻辑强行绑定在一起 |
| 维护困难 | 要改日志格式,必须改动所有方法- |
| 扩展性差 | 新增切面需求(如权限校验),又得批量修改 |
静态代理虽然能缓解部分问题,但同样存在代理类数量膨胀、接口变动时需同时修改代理类和目标类的缺陷-。
2.3 AOP的设计初衷
AOP的诞生正是为了解决上述问题。它的核心思想是:将横切关注点(日志、事务、安全等)从核心业务逻辑中抽离出来,形成独立的模块——切面(Aspect) ,在运行时动态织入-1。这样一来,业务代码只需专注于业务本身,横切逻辑统一管理,代码复用性和可维护性大幅提升。
三、核心概念讲解:切面(Aspect)
3.1 标准定义
Aspect(切面) :横切关注点的模块化实现,封装了通知(Advice)和切点(Pointcut),是AOP中的核心单元-2。
通俗地说,切面就是“在哪些位置(切点)做什么事(通知)”的封装。好比快递员有一个固定路线(切点),每次到达小区门口(连接点)都会做一件事——打电话通知取件(通知),这个“路线+动作”的组合就是一个切面。
3.2 AOP中的其他核心术语
Spring AOP涉及多个关键术语,下面逐一拆解-43:
| 术语 | 英文 | 一句话解释 |
|---|---|---|
| 连接点 | Join Point | 程序执行中可插入切面的点,Spring AOP中特指方法执行 |
| 切点 | Pointcut | 匹配连接点的条件,决定“哪些”连接点会被处理 |
| 通知 | Advice | 切面在特定连接点执行的动作,决定“做什么、何时做” |
| 目标对象 | Target Object | 被切面增强的原始业务对象 |
| 代理对象 | Proxy | Spring生成的包装对象,用于承载增强逻辑 |
| 织入 | Weaving | 将切面应用到目标对象并创建代理对象的过程 |
3.3 五种通知类型
Spring AOP提供了五种通知类型,各有不同的执行时机-4:
| 注解 | 类型 | 执行时机 |
|---|---|---|
@Before | 前置通知 | 目标方法执行前 |
@After | 后置通知 | 目标方法执行后(无论是否异常) |
@AfterReturning | 返回后通知 | 目标方法正常返回后 |
@AfterThrowing | 异常通知 | 目标方法抛出异常后 |
@Around | 环绕通知 | 目标方法执行前后,可控制执行流程 |
💡 重点掌握:@Around是最强大的通知类型,它能够完全控制目标方法的执行时机,甚至决定是否执行原方法-53。
四、关联概念讲解:切点(Pointcut)与切入点表达式
4.1 标准定义
Pointcut(切点) :通过表达式定义一组连接点的匹配规则,决定哪些连接点会被切面处理-。如果说通知定义了“做什么”,那么切点就定义了“在何处做”。
4.2 切点与连接点的关系
连接点(Join Point) :所有可以被增强的方法——静态存在的“候选者”。
切点(Pointcut) :筛选连接点的条件——决定哪些候选者最终被选中。
用一个表格帮助区分:
| 概念 | 作用 | 类比 |
|---|---|---|
| 连接点 | 可被拦截的位置(全量) | 所有学生 |
| 切点 | 筛选条件(匹配规则) | 成绩>90分 |
| 最终拦截目标 | 切点匹配到的连接点 | 筛选后的学生 |
4.3 切入点表达式(核心考点)
Spring AOP使用AspectJ的切入点表达式语言,最常用的是execution表达式-1:
基本格式:
execution(修饰符? 返回值 包名.类名.?方法名(参数) 异常?)常用通配符:
| 符号 | 含义 |
|---|---|
| 匹配任意字符,但只能匹配一个元素 |
.. | 匹配任意字符,可匹配多个元素(参数中表示任意参数) |
+ | 匹配指定类及其子类 |
常见示例:
| 表达式 | 含义 |
|---|---|
execution( com.example.service..(..)) | 匹配service包下所有类的所有方法 |
execution(public (..)) | 匹配所有公共方法 |
execution( com.example.service.UserService+.(..)) | 匹配UserService及其子类的所有方法 |
@annotation(com.example.Log) | 匹配被@Log注解标记的方法 |
📌 一句话记忆:连接点是“所有可拦截的地方”,切点是“从中选出一部分”的筛选条件。
五、概念关系与区别总结:AOP vs OOP
5.1 AOP与OOP的关系
很多人误以为AOP要取代OOP,这是一个常见误区。实际上,AOP是OOP的补充而非替代-22。
| 维度 | OOP(面向对象编程) | AOP(面向切面编程) |
|---|---|---|
| 关注点 | 纵向的对象结构 | 横向的横切关注点 |
| 核心单元 | 对象(Object) | 切面(Aspect) |
| 解决什么问题 | 实体建模、功能划分 | 日志、事务、权限等共性逻辑的抽取 |
| 封装方式 | 类继承、多态 | 动态代理、织入 |
5.2 一句话概括
OOP处理“是什么”的问题(对象的属性和行为),AOP处理“做什么之前/之后”的问题(横切逻辑的抽取与复用)。两者相辅相成,共同提升代码质量。
六、代码实战:从静态代理到Spring AOP
6.1 第一步:添加依赖
在Spring Boot项目中,只需在pom.xml中添加AOP Starter依赖,Spring Boot已提供自动配置支持-31:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
6.2 第二步:定义一个切面类
下面实现一个完整的日志记录切面,展示五种通知类型的使用-31:
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.; import org.springframework.stereotype.Component; @Aspect // ① 标记为切面类 @Component // ② 纳入Spring容器管理 public class LogAspect { // ③ 定义切点:匹配service包下所有类的所有方法 @Pointcut("execution( com.example.service..(..))") public void servicePointcut() {} // 前置通知:方法执行前执行 @Before("servicePointcut()") public void logBefore() { System.out.println("【前置】方法即将执行"); } // 后置通知:方法执行后执行(无论是否异常) @After("servicePointcut()") public void logAfter() { System.out.println("【后置】方法执行完毕"); } // 环绕通知:最强大,可完全控制方法执行 @Around("servicePointcut()") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); System.out.println("【环绕前】方法开始,参数:" + joinPoint.getArgs()); Object result = joinPoint.proceed(); // ④ 执行目标方法 long elapsed = System.currentTimeMillis() - start; System.out.println("【环绕后】方法结束,耗时:" + elapsed + "ms"); return result; } }
6.3 第三步:业务代码(完全无侵入)
@Service public class UserService { // 业务代码中没有任何日志相关代码 public User getUserById(Long id) { // 核心业务逻辑 return new User(id, "张三"); } }
6.4 执行效果
调用userService.getUserById(1L)时,控制台输出:
【环绕前】方法开始,参数:[1] 【前置】方法即将执行 【环绕后】方法结束,耗时:2ms 【后置】方法执行完毕
🎯 对比总结:静态代理时代码冗余、维护困难-;Spring AOP让业务代码干净纯粹,横切逻辑集中管理,新增切面需求时只需新增切面类,业务代码零改动。
七、底层原理:Spring AOP的动态代理机制
7.1 核心依赖:代理模式
Spring AOP的实现本质上依赖于代理模式(Proxy Pattern) 。代理模式通过引入代理对象作为目标对象的中间层,在调用目标方法前后插入增强逻辑,从而实现AOP-11。
Spring AOP的核心思想是:在运行时动态创建目标对象的代理对象,将通知织入代理对象的方法调用中,容器最终注入的是代理对象而非原始对象。
7.2 JDK动态代理 vs CGLIB
Spring AOP底层提供两种动态代理实现方式-4-12:
| 对比维度 | JDK动态代理 | CGLIB |
|---|---|---|
| 代理方式 | 基于接口 | 基于继承 |
| 必要条件 | 目标类必须实现至少一个接口 | 目标类不能是final类 |
| 实现原理 | 通过反射动态生成接口实现类 | 通过字节码技术生成目标类的子类 |
| final方法/类 | 无影响 | final方法/类无法被代理 |
| 性能 | 略低 | 更高 |
7.3 Spring的选择策略
Spring AOP的代理选择逻辑如下-1:
如果目标对象实现了接口 → 默认使用JDK动态代理
如果目标对象没有实现接口 → 使用CGLIB
可通过配置强制使用CGLIB:
@EnableAspectJAutoProxy(proxyTargetClass = true)
7.4 底层支撑:反射机制
JDK动态代理的核心依赖于Java的反射机制。java.lang.reflect.Proxy类配合InvocationHandler接口,在运行时动态生成代理类,通过Method.invoke()反射调用目标方法-12。Spring AOP正是基于这一机制,在运行时将横切逻辑动态织入。
八、高频面试题与参考答案
Q1:什么是AOP?谈谈你的理解
参考答案:AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它能够在不修改业务代码的前提下,为方法统一添加横切逻辑(如日志、事务、权限)。AOP通过动态代理机制,在方法执行前后织入增强代码-53。
踩分点:不修改业务代码、横切逻辑、动态代理、织入增强
Q2:JDK动态代理和CGLIB有什么区别?Spring如何选择?
参考答案:JDK动态代理基于接口实现,要求目标类必须实现接口,通过反射机制动态生成代理类;CGLIB基于继承实现,通过字节码技术生成目标类的子类,因此final类/方法无法被CGLIB代理。Spring的默认策略是:目标类有接口则用JDK,无接口则用CGLIB,可通过proxyTargetClass=true强制使用CGLIB-52。
踩分点:接口vs继承、反射vs字节码、final限制、选择策略
Q3:@Transactional为什么有时会失效?
参考答案:@Transactional失效的常见原因有三个:①方法不是public(事务只对public方法生效);②在同一个类中内部调用(没有经过代理对象);③方法被声明为final(无法被代理)-53。内部调用是最容易被忽视的原因——AOP依赖代理,而this.method()调用不会走代理对象。
踩分点:public限制、内部调用绕开代理、final限制
Q4:Spring AOP和AspectJ有什么区别?
参考答案:Spring AOP是运行时动态代理,通过JDK Proxy或CGLIB在运行时生成代理对象,仅支持方法级别的连接点;AspectJ是编译时或类加载时织入,支持字段、构造器等更丰富的连接点,性能更高。日常业务开发中Spring AOP已足够,复杂场景可考虑AspectJ-4。
踩分点:运行时vs编译时、方法级vs丰富连接点、性能差异
Q5:@Around和@Before/@After的区别?
参考答案:@Before和@After仅能在方法执行前/后插入逻辑,无法控制方法是否执行;@Around是最强大的通知类型,通过ProceedingJoinPoint.proceed()完全控制目标方法的执行时机,甚至可以决定是否执行原方法。例如权限校验失败时,可以直接返回而不调用proceed()-53。
踩分点:@Around可控制执行流程、proceed()是关键
九、结尾总结
回顾全文核心知识点
本文系统梳理了Spring AOP的完整知识链路:
| 模块 | 核心要点 |
|---|---|
| 为什么需要AOP | 解决传统方式代码冗余、耦合高、维护困难的问题 |
| 核心概念 | 切面=切点+通知;连接点与切点的区分是理解关键 |
| AOP vs OOP | AOP是OOP的补充而非替代,两者关注维度不同 |
| 通知类型 | 五种通知,@Around最强大,需掌握proceed()用法 |
| 底层原理 | 动态代理(JDK/CGLIB),Spring根据接口情况自动选择 |
| 面试考点 | 动态代理区别、事务失效原因、与AspectJ对比等 |
重点提示与易错点
⚠️ 内部调用不生效:同一个类中
this.method()调用AOP方法,不会经过代理对象⚠️ final限制:CGLIB代理时,final类和final方法无法被代理
⚠️ public必须:事务注解只对public方法生效
⚠️ 连接点≠切点:连接点是所有可拦截的位置,切点是筛选条件
下篇预告
本文聚焦于AOP的核心概念与原理。下一篇将深入探讨Spring AOP的源码实现,包括AOP代理对象的创建流程、通知执行的责任链模式,以及AspectJ注解驱动的解析机制,敬请期待!