02-02-Spring-AOP-Proxy
Spring AOP 代理机制
本文承接
02-01-Spring-AOP.md,只聚焦一个问题:Spring AOP 的代理对象到底如何工作。
主线是:为什么需要代理 -> 代理对象是什么 -> JDK/CGLIB 如何选择 -> 方法调用如何进入增强链 -> 哪些场景会绕过代理。
一、先建立主线:Spring AOP 为什么依赖代理
Spring AOP 的核心不是“修改业务类”,而是在运行期把目标 Bean 包装成代理 Bean。
业务代码看起来调用的是 Service:
1 | userService.createUser(request); |
但如果这个 Bean 命中了切面、事务、缓存等增强,容器中暴露给调用方的通常不是原始对象,而是代理对象:
1 | 调用方 |
所以 Spring AOP 最重要的一句话是:
只有调用经过代理对象,Spring AOP 才有机会在目标方法前后织入增强逻辑。
这句话可以解释后面大部分问题:
- 为什么自调用会失效。
- 为什么
private方法无法被增强。 - 为什么构造器里调用增强方法不生效。
- 为什么 JDK 动态代理按实现类注入可能失败。
- 为什么
final类和final方法会影响 CGLIB 代理。
Spring AOP 选择代理模式,主要是为了和 IOC 容器天然结合:Bean 创建完成后,容器可以统一决定是否把原始 Bean 替换成代理 Bean。
二、代理对象、目标对象与调用入口
理解 Spring AOP 前,先区分两个对象:
| 名称 | 含义 |
|---|---|
Target |
原始业务对象,也就是开发者写的类实例 |
Proxy |
Spring 根据目标对象和增强逻辑创建出来的代理对象 |
例如:
1 | @Service |
UserService 的原始实例就是 Target。如果它命中了某个切面,Spring 最终注入给其他 Bean 的可能是 Proxy。
可以打印运行时类型验证:
1 | System.out.println(userService.getClass()); |
可能输出:
1 | class jdk.proxy2.$Proxy58 |
也可能输出:
1 | class com.example.UserService$$SpringCGLIB$$0 |
前者是 JDK 动态代理,后者是 CGLIB 代理。
this 与 target 的区别
AOP 表达式里经常看到:
1 | @Pointcut("this(com.example.UserService)") |
两者关注点不同:
| 表达式 | 判断对象 |
|---|---|
this |
当前代理对象的类型 |
target |
被代理的目标对象类型 |
在 CGLIB 代理中,代理对象是目标类子类,所以 this 和 target 往往都容易匹配。
在 JDK 动态代理中,代理对象只实现接口,不继承目标实现类,所以 this(实现类) 可能匹配不上。
三、JDK 动态代理与 CGLIB 代理
Spring AOP 常用两种代理技术:
- JDK 动态代理:基于接口生成代理对象。
- CGLIB 代理:基于目标类生成子类代理对象。
JDK 动态代理
JDK 动态代理要求目标类至少实现一个接口。
示例:
1 | public interface UserService { |
生成的代理对象大致是:
1 | class jdk.proxy2.$Proxy58 implements UserService |
它的特点是:
- 代理对象实现接口。
- 代理对象不继承目标实现类。
- 调用入口是
InvocationHandler#invoke()。
因此下面这种注入方式更稳妥:
1 | private final UserService userService; |
而不是:
1 | private final UserServiceImpl userService; |
如果 Spring 使用的是 JDK 动态代理,按实现类注入可能失败,因为代理对象不是实现类的子类。
CGLIB 代理
CGLIB 代理不要求目标类实现接口,它通过生成目标类子类来完成代理。
例如:
1 | UserService$$SpringCGLIB$$0 extends UserService |
它的特点是:
- 代理对象继承目标类。
- 可以代理没有接口的类。
- 调用入口是 CGLIB 的
MethodInterceptor#intercept()。 - 无法代理
final类和final方法。
两种代理方式对比
| 维度 | JDK 动态代理 | CGLIB 代理 |
|---|---|---|
| 生成方式 | 基于接口生成代理类 | 基于目标类生成子类 |
| 是否需要接口 | 需要 | 不需要 |
| 代理对象类型 | 接口实现类 | 目标类子类 |
| 方法入口 | InvocationHandler#invoke() |
MethodInterceptor#intercept() |
| 典型限制 | 不能按实现类注入 | 不能代理 final 类 / 方法 |
Spring 如何选择代理方式
核心判断逻辑可以简化为:
1 | 如果配置了 proxyTargetClass=true |
常见强制 CGLIB 的方式:
1 | @EnableAspectJAutoProxy(proxyTargetClass = true) |
Spring Boot 中也可以通过配置控制:
1 | spring: |
四、代理方法调用链是怎么执行的
代理对象创建完成后,真正发生在运行期的是“方法调用被代理拦截”。
两种代理入口不同,但最终都会进入 Spring AOP 的拦截器链。
JDK 动态代理调用链
JDK 动态代理的入口是:
1 | JdkDynamicAopProxy#invoke(Object proxy, Method method, Object[] args) |
简化调用链如下:
1 | 调用接口方法 |
关键点是:代理对象不会直接调用目标方法,而是先拿到当前方法对应的拦截器链。
CGLIB 代理调用链
CGLIB 代理入口是:
1 | DynamicAdvisedInterceptor#intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) |
简化调用链如下:
1 | 调用代理子类方法 |
因此,不管是 JDK 动态代理还是 CGLIB,最终运行模型都是:
1 | 代理入口 |
五、代理对象什么时候创建
代理对象通常不是在方法第一次调用时才创建的,而是在 Bean 生命周期中提前创建。
典型时机是 Bean 初始化之后:
1 | Bean 实例化 |
源码入口在下一篇重点展开:
1 | AbstractAutoProxyCreator#postProcessAfterInitialization() |
所以本文只需要记住:
02-02讲代理对象如何工作,02-03-Spring-AOP-AutoProxyCreator.md讲代理对象如何被 Spring 自动创建。
六、代理失效的典型场景
代理失效不要死记场景,统一按一个判断标准看:
这次调用有没有经过 Spring 暴露出来的代理对象?
如果没有经过代理对象,AOP 就没有机会拦截。
自调用为什么会失效
示例:
1 | @Service |
外部调用 outer() 时,调用链是:
1 | 调用方 |
this.inner() 的 this 是目标对象自身,不是代理对象,所以不会再次进入代理逻辑。
这就是自调用失效的根本原因。
如何解决自调用失效
推荐优先拆分职责:
1 | @Service |
这样调用链重新经过代理对象:
1 | UserFacade |
也可以注入自身代理,但不如拆分职责清晰:
1 | @Service |
还可以使用 AopContext.currentProxy(),但它会让业务代码强依赖 Spring AOP,不建议作为常规方案。
final、private 与构造器调用限制
CGLIB 代理通过子类重写方法实现拦截,因此:
final类无法被继承,不能生成 CGLIB 子类代理。final方法无法被子类重写,不能被 CGLIB 拦截。private方法不能被子类重写,也不是外部代理调用入口。- 构造器执行时代理对象还没创建完成,构造器中调用增强方法不会触发 AOP。
示例:
1 | @Service |
init() 在构造器里被调用时,代理对象尚未完成创建,因此不会触发事务增强。
代理与类型注入问题
如果目标类实现了接口,Spring 默认可能使用 JDK 动态代理:
1 | UserServiceImpl implements UserService |
此时代理对象不是 UserServiceImpl 类型。
所以推荐面向接口注入:
1 | private final UserService userService; |
避免按实现类注入:
1 | private final UserServiceImpl userService; |
如果确实需要按类代理,可以考虑强制 CGLIB,但更推荐从设计上面向接口依赖。
七、调试、Demo 与面试总结
调试代理对象的几个方法
打印代理类名:
1 | System.out.println(bean.getClass()); |
判断是否是 AOP 代理:
1 | AopUtils.isAopProxy(bean); |
获取目标类型:
1 | AopUtils.getTargetClass(bean); |
建议断点:
1 | JdkDynamicAopProxy#invoke() |
一个最小可验证 Demo
Service:
1 | @Service |
Aspect:
1 | @Aspect |
Runner:
1 | @Component |
观察重点:
- 外部调用
outer()会触发 AOP。 outer()内部直接调用inner()不会再次触发 AOP。- 打印出来的
userService.getClass()不是普通业务类。
面试高频快答
Spring AOP 为什么使用代理?
Spring AOP 通过运行期代理对象在目标方法前后插入增强逻辑,不直接修改业务类字节码。
JDK 动态代理和 CGLIB 的区别?
JDK 动态代理基于接口生成代理对象,CGLIB 基于目标类生成子类代理;JDK 代理不能按实现类注入,CGLIB 不能代理
final类和final方法。
Spring 如何选择代理方式?
如果强制
proxyTargetClass=true则使用 CGLIB;否则有接口时默认优先 JDK 动态代理,没有接口时使用 CGLIB。
为什么自调用会导致 AOP 失效?
因为自调用是目标对象内部通过
this调用,没有经过 Spring 暴露的代理对象。
为什么构造器里调用 AOP 方法不生效?
构造器执行时 Bean 还没有完成生命周期处理,代理对象尚未创建或尚未暴露,调用不会经过代理。
源码阅读路线
建议按下面顺序阅读:
1 | AopProxy |
读源码时抓住两个问题:
- 代理对象是 JDK 还是 CGLIB 创建的。
- 方法调用如何进入
MethodInterceptor链。
本篇总结
本文只讲代理机制本身,可以总结成一条链路:
1 | 目标对象 Target |
必须掌握的核心点:
- Spring AOP 生效的前提是调用经过代理对象。
- JDK 动态代理基于接口,CGLIB 基于目标类子类。
- 自调用、
private、final、构造器调用都可能绕过或限制代理。 - 代理对象创建时机属于自动代理创建器的职责,下一篇继续展开。
