本文发布于北京时间


一、开篇引入


二、痛点切入:为什么需要AOP

2.1 传统实现方式:手动埋点
public class UserService { public void createUser(String name) { // 手动埋点日志——每个方法都要写一遍 System.out.println("[LOG] 开始执行 createUser, 参数: " + name); long start = System.currentTimeMillis(); // 核心业务逻辑 System.out.println("创建用户: " + name); // 手动埋点性能监控——每个方法都要写一遍 System.out.println("[LOG] 执行完毕, 耗时: " + (System.currentTimeMillis() - start) + "ms"); } public void deleteUser(Long id) { // 同样冗长的日志代码,完全重复 System.out.println("[LOG] 开始执行 deleteUser, 参数: " + id); // ... } }
2.2 传统方式的四大痛点
| 痛点 | 说明 |
|---|---|
| 代码冗余 | 日志、事务、权限校验等横切逻辑在每个方法中重复出现 |
| 耦合过高 | 业务代码与非业务代码混杂,核心逻辑被淹没 |
| 维护困难 | 修改日志格式需要在数十个甚至上百个方法中逐一修改 |
| 扩展性差 | 新增一个横切关注点(如安全审计),需要改动所有目标方法 |
2.3 AOP的设计初衷
AOP的核心价值在于:将横切关注点(Cross-cutting Concerns,如日志、事务、安全、缓存)从业务逻辑中横向抽离,实现关注点分离。开发者只需要编写一次切面逻辑,即可应用到所有目标方法上,业务代码中不再掺杂非核心逻辑。
三、核心概念讲解:AOP(概念A)
3.1 标准定义
AOP,全称 Aspect Oriented Programming(面向切面编程) ,是一种编程范式。它通过“切面”将跨越多个模块的通用功能(日志、事务、权限等)从业务逻辑中抽取出来,实现横切关注点的模块化-。
3.2 拆解关键词
切面:一个独立的模块,封装了横切关注点的全部逻辑
横切关注点:那些分散在各个业务模块中、不属于核心业务但又不可或缺的功能
织入:将切面代码插入到目标方法执行流程中的过程
3.3 生活化类比
想象一座大型购物中心(业务系统)。OOP把每个店铺当作独立的“类”,每家店有自己的收银系统、安保、保洁。AOP则像商场的统一物业管理——安保巡逻、保洁清扫、中央空调由物业统一负责,所有店铺共享这套基础设施,每家店铺不需要自己雇保安、配保洁-。
3.4 核心术语速查表
| 术语 | 英文 | 含义 |
|---|---|---|
| 切面 | Aspect | 横切关注点的模块化,如日志切面、事务切面 |
| 连接点 | Join Point | 程序执行过程中能够插入切面的点,Spring AOP中特指方法执行 |
| 通知 | Advice | 切面的具体行为(在何时做什么事) |
| 切点 | Pointcut | 定义通知应用到哪些连接点上的表达式 |
| 目标对象 | Target Object | 被切面增强的原始对象 |
| 代理对象 | Proxy | 运行时生成的对象,包含增强逻辑 |
| 织入 | Weaving | 将切面应用到目标对象并创建代理对象的过程 |
四、关联概念讲解:动态代理(概念B)
4.1 标准定义
动态代理是Spring AOP的底层实现机制。它指在程序运行时,Java动态创建目标对象的代理对象,在代理对象的方法执行中嵌入切面逻辑,从而实现对目标对象的增强-。
4.2 概念A与概念B的关系
AOP是一种编程思想,动态代理是该思想在Spring框架中的具体实现手段。
AOP(思想):告诉你“要把横切逻辑抽出来”
动态代理(手段):告诉计算机“怎么在运行时把切面插进去”
4.3 两种动态代理方式对比
| 维度 | JDK动态代理 | CGLIB |
|---|---|---|
| 底层原理 | 基于接口的代理,使用java.lang.reflect.Proxy | 基于继承的代理,通过字节码技术生成目标类的子类- |
| 代理范围 | 只能代理实现了接口的类- | 可以代理接口和普通类 |
| Spring选择策略 | 目标类有接口时默认使用 | 目标类无接口时使用 |
| 版本差异 | Spring 5.2+默认启用objenesis避免调用目标构造器- | 始终可用 |
4.4 运行机制简示
// 用户调用 userService.createUser("张三"); ↓ // 实际上调用的是代理对象 UserServiceProxy.createUser("张三"); ↓ // 代理对象执行增强逻辑 [Before Advice] 前置通知:记录开始时间、日志 ↓ // 调用原始目标方法 target.createUser("张三"); ↓ [After Advice] 后置通知:记录结束时间、性能统计
五、概念关系与区别总结
5.1 一句话概括
AOP是“做什么”的设计思想,动态代理是“怎么做”的技术实现;AOP指导代码组织方式,动态代理提供了运行时织入的能力。
5.2 对比记忆表
| 对比维度 | AOP(思想) | 动态代理(实现) |
|---|---|---|
| 定位 | 编程范式 | 技术手段 |
| 关注点 | 代码如何组织(切面、连接点、通知) | 代理对象如何生成 |
| 与Spring关系 | Spring两大核心特性之一 | Spring AOP的底层支柱 |
六、代码示例演示
6.1 完整可运行示例:接口访问日志切面
// 1. 定义切面类 @Aspect // 标注这是一个切面类 @Component // 交由Spring容器管理 public class LogAspect { private static final Logger log = LoggerFactory.getLogger(LogAspect.class); // 2. 定义切点:匹配com.example.controller包下所有类的所有方法 @Pointcut("execution( com.example.controller..(..))") public void controllerMethods() {} // 3. 前置通知:方法执行前记录请求信息 @Before("controllerMethods()") public void logBefore(JoinPoint joinPoint) { log.info("[请求开始] 方法: {}, 参数: {}", joinPoint.getSignature().getName(), Arrays.toString(joinPoint.getArgs())); } // 4. 环绕通知:同时记录执行耗时 @Around("controllerMethods()") public Object logAround(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); // 调用目标方法 long elapsed = System.currentTimeMillis() - start; log.info("[请求完成] 方法: {}, 耗时: {}ms", pjp.getSignature().getName(), elapsed); return result; } }
6.2 执行流程解析
Spring容器启动时,扫描到
@Aspect标注的LogAspect类对于匹配切点表达式
execution( com.example.controller..(..))的目标Bean,Spring不会直接返回原始对象Spring通过动态代理创建一个代理对象,代理对象持有原始对象的引用
当调用代理对象的方法时,代理对象按照
@Before→@Around前半部分 → 原始方法 →@Around后半部分 →@After的顺序执行用户最终拿到的是代理对象,从而获得了日志增强能力
七、底层原理与技术支撑
7.1 核心技术栈
| 技术 | 作用 |
|---|---|
| JDK动态代理 | 代理实现了接口的类,基于java.lang.reflect.Proxy和InvocationHandler- |
| CGLIB | 代理没有接口的类,通过字节码技术动态生成子类- |
| 反射 | JDK动态代理依赖反射调用目标方法 |
| AspectJ表达式 | Spring AOP的切点表达式借用了AspectJ的语法,但底层仍是动态代理 |
7.2 代理创建流程
Spring在Bean的初始化后置处理阶段完成代理的创建:
Bean实例化 → 属性注入 → 初始化(InitializingBean/@PostConstruct)→ ↓ BeanPostProcessor.postProcessAfterInitialization() ↓ AbstractAutoProxyCreator判断是否需要AOP代理 ↓ 需要代理 → 选择代理方式(JDK/CGLIB)→ 生成代理对象 ↓ 返回代理对象 → 放入容器
7.3 为什么Spring AOP不支持方法内部调用
这是面试中的经典问题。原因在于:当目标对象内部直接调用this.method()时,调用的是原始对象的方法,而非代理对象的方法,因此切面不会生效。解决方案:通过AopContext.currentProxy()获取代理对象后再调用,或者将方法拆分到不同的Bean中。
八、高频面试题与参考答案
面试题1:Spring AOP的底层原理是什么?
参考答案:Spring AOP基于动态代理实现。当目标类实现了接口时,默认使用JDK动态代理,通过java.lang.reflect.Proxy和InvocationHandler生成代理对象;当目标类没有实现接口时,使用CGLIB,通过字节码技术生成目标类的子类作为代理对象-。Spring在Bean初始化完成后,通过BeanPostProcessor机制判断是否需要创建代理对象。
踩分点:动态代理 + JDK/CGLIB两种方式 + 选择策略 + BeanPostProcessor时机
面试题2:JDK动态代理和CGLIB有什么区别?
参考答案:
原理不同:JDK动态代理基于接口代理,要求目标类实现接口;CGLIB基于继承代理,通过生成子类实现-
性能差异:JDK动态代理依赖反射调用,CGLIB直接调用父类方法,CGLIB性能略优
使用限制:JDK只能代理接口;CGLIB无法代理final类和final方法
踩分点:对比完整 + 各点一句话说清
面试题3:Spring AOP和AspectJ有什么区别?
参考答案:
实现层次不同:Spring AOP基于运行时动态代理,只支持方法级别的拦截;AspectJ通过编译时/类加载时字节码织入,支持字段、构造器等更细粒度的拦截-
性能差异:AspectJ运行时性能更高,无反射开销-
集成方式:Spring AOP与Spring IoC无缝集成,配置简单;AspectJ功能更强但配置更复杂
踩分点:运行时vs编译时 + 方法级vs多级别 + 适用场景建议
面试题4:Spring AOP的通知类型有哪些?
参考答案:
@Before:前置通知,在目标方法执行前执行
@AfterReturning:后置通知,在目标方法正常返回后执行
@AfterThrowing:异常通知,在目标方法抛出异常后执行
@After:最终通知,无论是否异常都会执行
@Around:环绕通知,最强大的通知类型,可控制目标方法的执行前后-
踩分点:五种全列 + 每种一句话说明 + Around最常用
面试题5:Spring AOP中为什么方法内部调用无法被切面增强?
参考答案:因为Spring AOP基于代理实现。当目标对象内部直接调用this.method()时,调用的是原始对象而非代理对象,切面逻辑不会触发。解决方法:1)将目标方法拆分到不同的Bean中;2)通过AopContext.currentProxy()获取当前代理对象后调用-。
踩分点:点明原因(this指向原始对象)+ 给出解决方案
九、结尾总结
9.1 核心知识回顾
AOP是一种编程思想,通过横切关注点的模块化实现关注点分离
动态代理是Spring AOP的底层实现手段,包含JDK动态代理和CGLIB两种方式
理解概念间关系:AOP(思想)→ 动态代理(实现)→ 切面(模块)→ 通知/切点(具体要素)
常见应用场景:日志记录、性能监控、事务管理、权限校验、缓存处理
9.2 进阶预告
本文聚焦于Spring AOP的基础原理与高频考点。下一篇将深入Spring AOP在事务传播机制中的具体应用,剖析@Transactional注解的底层运作原理,以及PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW等七种传播行为在源码层面的实现差异。欢迎持续关注本系列更新。