跳到主要内容Spring AOP 详解:代理模式与动态代理核心原理 | 极客日志Javajava
Spring AOP 详解:代理模式与动态代理核心原理
Spring AOP 的核心原理,重点阐述代理模式与动态代理机制。内容涵盖静态代理与动态代理的区别,JDK 动态代理与 CGLIB 动态代理的实现步骤及适用场景。深入解析 Spring AOP 源码逻辑,包括 ProxyFactory 代理工厂、proxyTargetClass 配置及 JDK/CGLIB 切换策略。同时对比了 AOP 与拦截器、统一异常处理器的区别,明确了在不同场景下(如 Service 层增强、细粒度控制、异常处理)的最佳实践原则,帮助开发者在不修改业务代码的前提下实现功能增强。
活在当下5 浏览 1. 定义
为目标对象提供'代理类',让调用方不直接访问目标对象,而是通过代理类间接访问,从而在代理类中实现功能增强(比如日志、权限校验)。
2. 核心角色(以'房屋租赁'为例)
| 角色 | 对应示例 | 作用说明 |
|---|
| Subject | HouseSubject接口 | 定义目标对象和代理类的共同行为(比如'租房') |
| RealSubject | RealHouseSubject(房东) | 目标对象(被代理的实际业务执行者) |
| Proxy | HouseProxy(中介) | 代理类,包装目标对象,在调用目标方法前后添加增强逻辑 |
静态代理的实现步骤(以房屋租赁为例)
1. 定义共同接口(Subject)
2. 实现目标对象(RealSubject)
3. 实现代理类(Proxy)
4. 使用代理
静态代理的核心特点
- 提前创建:代理类的.class 文件在程序运行前就已存在(比如 HouseProxy 是提前写好的);
- 功能增强:不修改目标对象代码,通过代理类实现'附加逻辑'(符合'开闭原则');
- 局限性:一个代理类只能代理一种类型的目标对象,若要代理多个类,需写多个代理类(所以 Spring AOP 实际用的是动态代理,解决这个局限性)。
和 Spring AOP 的关系
静态代理是代理模式的基础,而 Spring AOP 的底层是动态代理(运行时自动生成代理类),但核心思想一致:通过代理类包装目标对象,实现无侵入的功能增强。
二、动态代理的核心优势
相比静态代理,动态代理无需为每个目标对象写单独的代理类,而是在程序运行时动态生成代理对象,更灵活、更通用。
2.1 JDK 动态代理的实现步骤(以房屋租赁为例)
步骤 1:准备'干活的对象'(目标对象)
- 目标:确定'要被代理的实体 + 要干的活';
- 人话:找个'房东'(RealHouseSubject),他会干'租房'(rentHouse 方法)这个活;
- 代码对应:
步骤 2:写'中介的规则'(InvocationHandler)
- 目标:定义'中介要在房东干活前后加什么额外操作';
- 人话:中介(JDKInvocationHandler)要在房东租房前说'开始代理',租房后说'代理结束';
- 代码对应:
步骤 3:生成'中介'(动态代理对象)
- 目标:让 JVM 自动创建'中介',代替房东和用户打交道;
- 人话:告诉 JVM'用房东的类加载器、房东会的活(接口)、中介的规则',生成一个中介;
- 代码对应:
步骤 4:看结果(验证逻辑)
3. JDK 动态代理的关键 API
| API | 作用说明 |
|---|
InvocationHandler | 方法调用处理器接口,invoke 方法是代理增强逻辑的入口(参数含代理对象、目标方法、方法参数)。 |
Proxy.newProxyInstance | 动态生成代理对象的核心方法,需传入类加载器、目标对象实现的接口、InvocationHandler 实例。 |
4. JDK 动态代理的局限性
只能代理实现了接口的类(因为 newProxyInstance 需要传入接口数组)。若要代理无接口的类,需用CGLIB 动态代理(后续内容)。
5. 和 Spring AOP 的关系
Spring AOP 默认优先用JDK 动态代理(代理有接口的类),若目标类无接口,则自动切换为CGLIB 动态代理,底层逻辑与上述一致 —— 通过动态生成代理类实现无侵入的功能增强。
三、AOP + 代理模式核心拆解
第一层:先搞懂'目标对象'
核心逻辑
目标对象 = Spring 管理的 Bean(比如 Controller/Service) + 这个 Bean 里的业务方法(比如接口方法 / 保存用户方法)
解释
- 干活的对象:UserController 实例(Spring 自动创建的 Bean);
- 具体的活:getUserById(Long id) 方法(查询用户的业务逻辑)。
第二层:为什么需要'代理'(AOP 的本质)
核心逻辑
不想修改目标对象的代码,但想给'具体的活'加额外逻辑(日志 / 权限 / 事务)→ 找个'中间人'(代理对象)帮我们加。
解释
比如:房东(目标对象)只想'租房'(核心活),不想管'带看房 / 签合同'(额外活)→ 找中介(代理对象),中介帮房东加这些额外活,房东只干核心的租房。
第三层:「静态代理」vs「动态代理」(代理的两种方式)
| 类型 | 核心逻辑 | 人话解释 |
|---|
| 静态代理 | 提前写死代理类(一个目标对象对应一个代理类) | 给房东 A 写个专属中介,给房东 B 写另一个专属中介 |
| 动态代理 | 运行时自动生成代理类(所有目标对象共用一套逻辑) | 一个中介公司,能代理所有房东,不用单独写中介 |
关键:Spring AOP 用的是'动态代理'
- JDK 动态代理:代理有接口的类(比如 Controller/Service 都实现了接口);
- CGLIB 动态代理:代理没接口的类(兜底方案)。
第四层:Spring AOP 的核心组件(代理的'包装')
把动态代理的逻辑封装成 3 个好懂的组件,不用自己写代理类:
1. 切面类(@Aspect + @Component)
- 核心逻辑:告诉 Spring'这是一个 AOP 增强类',并交给 Spring 管理;
- 人话:就是'中介公司的规则手册',定义了'要帮哪些房东干活 + 要加哪些额外活'。
2. 切点(execution/@annotation)
- 核心逻辑:划定'要增强哪些目标对象的方法'(哪些房东要被代理);
- 人话:规则 1:'只代理朝阳区的房东'(execution 匹配包 / 类);规则 2:'只代理贴了'急租'标签的房东'(@annotation 匹配注解)。
3. 通知(@Before/@Around 等)
- 核心逻辑:定义'要加的额外活'(什么时候加、加什么);
- 人话:规则:'带客户看房(@Before)→ 房东租房 → 收中介费(@After)'。
第五层:完整执行流程
以'访问 UserController 的 getUserById 方法'为例:
- 你调用 userController.getUserById(1) → 实际调用的是 Spring 生成的'代理对象';
- 代理对象先执行通知逻辑(比如 @Before 打印日志);
- 代理对象调用'目标对象'的 getUserById(1)(真正的查询逻辑);
- 代理对象再执行后续通知(比如 @After 统计耗时);
- 返回结果给你。
核心总结(一句话)
Spring AOP = 动态代理(自动生成中介) + 切面(中介规则) + 切点(选房东) + 通知(加额外活),最终实现'不修改目标对象代码,却能增强其方法'。
四、CGLIB 动态代理
1. 先搞懂:CGLIB 是干嘛的?
JDK 动态代理只能代理'有接口的类',而CGLIB 是用来代理'没有接口的类'(比如普通的 Java 类),它的原理是动态生成目标类的子类,通过子类来实现增强。
2. CGLIB 动态代理的 3 步核心动作
步骤 1:准备'要被代理的类'(不用接口!)
- 目标:找一个'没实现接口的类'(比如普通的房东类);
- 人话:房东类 RealHouseSubject 没实现任何接口,但会干'租房'这个活;
- 代码对应:
步骤 2:写'增强规则'(MethodInterceptor)
- 目标:定义'代理要在房东干活前后加什么操作';
- 人话:中介(CGLIBInterceptor)要在房东租房前说'开始代理',租房后说'代理结束';
- 代码对应:
步骤 3:生成'代理子类'(Enhancer.create)
- 目标:让 CGLIB 动态生成目标类的子类(代理类);
- 人话:告诉 CGLIB'要代理的类 + 增强规则',生成一个'房东的子类(中介)';
- 代码对应:
步骤 4:看结果(和 JDK 代理一样)
3. CGLIB 和 JDK 代理的核心对比
| 维度 | JDK 动态代理 | CGLIB 动态代理 |
|---|
| 代理条件 | 必须实现接口 | 可以代理无接口的类(生成子类) |
| 核心 API | InvocationHandler + Proxy | MethodInterceptor + Enhancer |
| 底层原理 | 基于接口的动态代理 | 基于子类的动态代理 |
| Spring AOP 场景 | 优先用(代理有接口的 Bean) | 兜底用(代理无接口的 Bean) |
4. 总结 CGLIB
CGLIB 是 JDK 代理的'补位方案'—— 专门解决'无接口类的代理问题',通过生成目标类的子类,实现和 JDK 代理一样的增强效果,Spring AOP 会自动在 JDK 和 CGLIB 之间切换。
五、Spring AOP 的源码
1. Spring AOP 的核心逻辑:谁来生成代理?
Spring AOP 生成代理的核心类是 AnnotationAwareAspectJAutoProxyCreator,它的父类 AbstractAutoProxyCreator 里的 createProxy 方法,是生成代理的'入口'。
2. 生成代理的 3 步核心流程(源码逻辑)
步骤 1:创建'代理工厂'(ProxyFactory)
- 目标:封装代理的配置信息(比如用 JDK 还是 CGLIB);
- 代码对应:
步骤 2:决定'用 JDK 还是 CGLIB'(核心判断)
通过 proxyTargetClass 这个配置项,决定代理方式:
| proxyTargetClass 值 | 目标对象情况 | 代理方式 |
|---|
| false(默认) | 实现了接口 | JDK 代理 |
| false(默认) | 没实现接口 | CGLIB 代理 |
| true | 不管有没有接口 | CGLIB 代理 |
步骤 3:生成代理对象(调用 JDK/CGLIB 的 API)
代理工厂通过 getProxy 方法,最终调用我们之前学的 JDK/CGLIB API 生成代理:
- JDK 代理:调用 Proxy.newProxyInstance(对应 JdkDynamicAopProxy 类);
- CGLIB 代理:调用 Enhancer.create(对应 CglibAopProxy 类);
3. Spring Boot 中的关键配置
- Spring Boot 2.x 开始,默认用 CGLIB 代理(相当于默认 proxyTargetClass=true);
- 若要改回 JDK 代理,需在配置文件加:
4. 一句话总结 Spring AOP 源码逻辑
Spring AOP 通过 ProxyFactory 封装配置,根据 proxyTargetClass 和'目标类是否有接口',自动选择 JDK/CGLIB 代理,最终调用对应的动态代理 API 生成代理对象 ——我们之前学的 JDK/CGLIB,就是 Spring AOP 的底层实现。
你只写核心业务代码,想加额外功能时,不用改业务代码,只需要写 AOP 的切面 / 切点规则就行。
六、拆解 aop 思想
- 切面表达式(@Pointcut)只是选目标(选哪些业务 / 接口要加功能);
- 真正的'额外功能代码'(比如打日志、校验权限)要写在**@Before/@After/@Around**这些「通知」里;
- 整体逻辑是:写切面类 → 用切点选目标 → 用通知写额外功能,全程不用碰你的核心业务代码。
2. 分清:拦截器、统一异常、AOP 各自管啥
| 组件 | 角色定位 | 管的范围 | 核心作用 | 例子(对应你的接口开发) |
|---|
| 拦截器 | 餐厅门口的保安 | 只管「接口请求的进出」 | 拦截 HTTP 请求(进)/响应(出),管请求级的事 | 校验 token、限制接口访问频率、记录请求 URL |
| 统一异常 | 餐厅的售后客服 | 只管「全局异常兜底」 | 捕获所有未处理的异常,统一返回格式 | 接口抛空指针/数据库异常时,返回统一的{code:500,msg:'失败'} |
| AOP | 餐厅里的「万能服务员」 | 管「任意方法的任意时机」 | 给任意方法(接口/Service/工具类)加功能 | 给 Service 层加事务、给指定方法打详细日志、统计方法执行耗时 |
3. 关键:啥时候用 AOP?(对比拦截器 / 统一异常)
核心判断标准:当你要给「非接口级的方法」加额外功能,或者需要更细粒度的控制时,就用 AOP。
场景 1:拦截器管不到的地方,必须用 AOP
拦截器只能拦「Controller 层的 HTTP 请求」,但你后端的核心逻辑在 Service 层、Mapper 层,这些地方想加额外功能,只能用 AOP:
- 例子 1:想统计「用户下单 Service 方法」的执行耗时 → 拦截器拦不到 Service 方法,用 AOP 的@Around 包一下这个 Service 方法就行;
- 例子 2:想给「所有修改数据的 Mapper 方法」加日志(记录谁改了、改了啥)→ Mapper 层没有 HTTP 请求,拦截器没用,AOP 可以精准切这些 Mapper 方法。
场景 2:需要更细粒度的控制,用 AOP
拦截器是'要么拦整个接口,要么不拦',但 AOP 可以精准到「某个类的某个方法」「带某个注解的方法」:
- 例子 1:同一个 Controller 里,只想给「删除用户」方法加权限校验,其他方法不加 → 拦截器做不到(拦就全拦),AOP 的切点可以只切这个删除方法;
- 例子 2:想给「所有带 @Transactional 注解的方法」加事务执行日志 → 用 AOP 切带这个注解的方法就行,拦截器做不到。
场景 3:拦截器 / AOP 都能做,但 AOP 更合适的情况
- 拦截器能打(记录请求 URL、参数),但只能拿到 HTTP 层面的信息;
- AOP 能打更细的日志(比如拿到 Service 层的入参、出参、方法名),还能区分'接口成功 / 失败'的日志格式,更灵活。
场景 4:统一异常和 AOP 的配合
统一异常是「兜底捕获所有异常」,但 AOP 可以「在异常发生时做额外处理」:
- 比如:接口抛异常后,除了统一返回错误信息,还想记录'异常方法名、参数、堆栈信息到专门的日志文件' → 用 AOP 的@AfterThrowing(异常通知)来做,统一异常只负责返回格式,AOP 负责记录详细异常日志。
4. 作为后端,记这 3 个'使用原则'就够了
- 管接口请求 / 响应 → 用拦截器:比如 token 校验、跨域、接口限流;
- 管全局异常格式 → 用统一异常处理器:比如@RestControllerAdvice + @ExceptionHandler;
- 管方法级的额外功能 → 用 AOP:比如 Service 层加日志 / 事务 / 耗时统计、精准切某个方法加权限 / 校验。
总结
- 核心:AOP 是「方法级」的增强,拦截器是「接口请求级」的增强,统一异常是「全局异常兜底」,三者分工不同、可以配合使用;
- 什么时候用 AOP:拦截器管不到(比如 Service/Mapper 层)、需要细粒度控制(比如只切某个方法)时,就用 AOP;
- 你的工作方式:核心业务代码不动,加额外功能时,接口级的用拦截器,方法级的用 AOP(写切面 + 切点 + 通知),异常兜底用统一异常。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online