一文讲透 Java 代理模式:从静态代理、JDK 动态代理到 Spring AOP

一文讲透 Java 代理模式:从静态代理、JDK 动态代理到 Spring AOP
YYT一文讲透 Java 代理模式:从静态代理、JDK 动态代理到 Spring AOP
代理模式是 Java 中非常重要的设计模式,也是理解 Spring AOP、Spring 事务、MyBatis Mapper 代理、RPC 远程调用等技术的基础。
很多人第一次看代理模式时,会觉得它很简单:不就是在真实对象外面包了一层吗?
这个理解没有错,但还不够深入。
代理模式真正重要的地方在于:它让我们可以在不修改原始业务代码的情况下,对方法调用过程进行增强。
比如原来的业务方法只负责保存用户:
1 | public void save(User user) { |
现在我们想在保存用户之前打印日志,在保存用户之后统计耗时。如果直接修改业务代码,当然可以实现,但这样日志、耗时统计、事务、权限等非核心逻辑就会和业务代码混在一起。
代理模式解决的就是这个问题。
它的核心调用链是:
1 | 调用方 -> 代理对象 -> 真实对象 |
调用方并不直接访问真实对象,而是先访问代理对象。代理对象可以在调用真实对象之前、之后,甚至异常时,插入额外逻辑。
代理模式到底解决了什么问题?
先看一个最普通的业务接口:
1 | public interface UserService { |
真实业务类:
1 | public class UserServiceImpl implements UserService { |
如果没有代理,调用方式很直接:
1 | UserService userService = new UserServiceImpl(); |
调用链就是:
1 | 调用方 -> UserServiceImpl.save() |
这时候如果需要加日志,最直接的做法是改业务类:
1 | public class UserServiceImpl implements UserService { |
这段代码虽然能跑,但问题很明显。
UserServiceImpl 的核心职责应该是保存用户,现在却混入了日志逻辑。以后如果还要加事务、权限校验、接口限流、耗时统计,业务方法会越来越臃肿。
更麻烦的是,如果 OrderService、ProductService、PaymentService 也都要加日志,那么每个业务类都要手动改一遍。
这就违反了一个很重要的原则:
1 | 核心业务逻辑和通用增强逻辑不应该强耦合在一起。 |
代理模式的价值就在这里。
它不直接修改真实对象,而是在真实对象外面包一层代理对象。
真实对象只负责核心业务。
代理对象负责日志、事务、权限、缓存等增强逻辑。
静态代理:最容易理解的代理模式
静态代理就是我们自己手动写一个代理类。
代理类和真实类实现同一个接口。
真实对象:
1 | public class UserServiceImpl implements UserService { |
代理对象:
1 | public class UserServiceProxy implements UserService { |
使用方式:
1 | UserService target = new UserServiceImpl(); |
这段代码里面最重要的是:
1 | private final UserService target; |
target 就是真实对象。
构造方法:
1 | public UserServiceProxy(UserService target) { |
表示创建代理对象时,把真实对象传进来。
所以:
1 | target.save(user); |
真正调用的是:
1 | UserServiceImpl.save(user); |
执行流程是:
1 | 1. 调用 proxy.save(user) |
也就是:
1 | 调用方 -> UserServiceProxy -> UserServiceImpl |
这就是静态代理。
静态代理的优点是简单、直观,非常适合理解代理模式的思想。
但是它的问题也很明显:每一个接口、每一个业务类,都可能需要写一个对应的代理类。
比如现在只有一个 UserService,还好处理。
如果系统里有很多服务:
1 | UserService |
那可能就要写很多代理类:
1 | UserServiceProxy |
而且这些代理类里面的增强逻辑可能还差不多,都是打印日志、统计耗时、开启事务、提交事务。
这样会产生大量重复代码。
所以静态代理适合学习代理模式,但在大型项目中,如果全靠手写代理类,维护成本会很高。
这时候就需要动态代理。
动态代理:代理类不再手写,而是在运行时生成
动态代理的核心思想是:
1 | 不用我们手写代理类,而是在程序运行时自动生成代理对象。 |
静态代理中,我们需要自己写:
1 | public class UserServiceProxy implements UserService { |
动态代理中,这个代理类不需要我们手写,而是由程序在运行时生成。
Java 中常见的动态代理主要有两种:
1 | 1. JDK 动态代理 |
这两种方式都可以生成代理对象,但它们的实现原理不同。
JDK 动态代理:基于接口生成代理对象
JDK 动态代理是 Java 原生提供的动态代理机制。
它有一个重要特点:
1 | 目标对象必须实现接口。 |
比如我们有一个接口:
1 | public interface UserService { |
真实对象实现这个接口:
1 | public class UserServiceImpl implements UserService { |
然后可以使用 JDK 动态代理创建代理对象:
1 | import java.lang.reflect.InvocationHandler; |
这段代码里最核心的是:
1 | Proxy.newProxyInstance(...) |
它会在运行时生成一个代理对象。
这个代理对象也实现了 UserService 接口,所以可以强转成:
1 | UserService proxy |
也就是说,调用方看到的依然是:
1 | UserService |
但实际上它拿到的是 JDK 运行时生成的代理对象。
当执行:
1 | proxy.save(new User()); |
不会直接进入 UserServiceImpl.save(),而是先进入:
1 | InvocationHandler.invoke() |
然后在 invoke() 方法里面,再通过反射调用真实对象:
1 | method.invoke(target, args); |
所以 JDK 动态代理的调用链是:
1 | 调用方 |
这里有几个参数需要理解一下。
第一个参数:
1 | target.getClass().getClassLoader() |
表示使用哪个类加载器来加载代理类。
第二个参数:
1 | target.getClass().getInterfaces() |
表示代理对象需要实现哪些接口。
第三个参数:
1 | new InvocationHandler() { ... } |
表示代理对象的方法调用逻辑。
你可以把 InvocationHandler 理解成代理对象的统一拦截器。
所有通过代理对象调用的方法,都会进入 invoke() 方法。
这就是为什么动态代理可以统一处理日志、事务、权限等逻辑。
JDK 动态代理为什么必须有接口?
这是因为 JDK 动态代理生成的代理类,本质上是实现了目标对象的接口。
比如目标类实现了:
1 | public class UserServiceImpl implements UserService |
JDK 动态代理生成的代理类,大概可以理解成这样:
1 | public class $Proxy0 implements UserService { |
当然真实生成的字节码比这个复杂,但思想可以这样理解。
代理对象和真实对象都实现了同一个接口:
1 | UserServiceImpl implements UserService |
所以调用方可以这样写:
1 | UserService userService = proxy; |
但是如果目标类没有实现接口,JDK 动态代理就不知道代理对象应该实现什么接口。
所以 JDK 动态代理要求目标对象必须实现接口。
CGLIB 动态代理:基于继承生成代理对象
CGLIB 和 JDK 动态代理不同。
JDK 动态代理是基于接口。
CGLIB 动态代理是基于继承。
也就是说,即使目标类没有实现接口,CGLIB 也可以生成代理对象。
比如这个类没有实现任何接口:
1 | public class UserService { |
CGLIB 可以在运行时生成它的子类代理对象。
可以理解成生成了一个类似这样的类:
1 | public class UserService$$EnhancerByCGLIB extends UserService { |
也就是说,CGLIB 的代理对象是目标类的子类。
调用链大概是:
1 | 调用方 |
这就是为什么 CGLIB 不要求目标类实现接口。
因为它不是靠“实现同一个接口”来代理,而是靠“继承目标类”来代理。
CGLIB 的限制
CGLIB 是基于继承实现的,所以它有几个天然限制。
第一,目标类不能是 final。
因为 final 类不能被继承。
1 | public final class UserService { |
这种类 CGLIB 无法代理。
第二,目标方法不能是 final。
因为 final 方法不能被子类重写。
1 | public class UserService { |
CGLIB 代理对象虽然可以继承 UserService,但不能重写 save() 方法。
而不能重写,就不能在方法前后插入增强逻辑。
第三,private 方法也不能被代理。
因为 private 方法对子类不可见,不能被重写。
所以 CGLIB 虽然不要求接口,但要求目标类和目标方法能够被继承、被重写。
JDK 动态代理和 CGLIB 的详细区别
JDK 动态代理和 CGLIB 动态代理最核心的区别是:
1 | JDK 动态代理:基于接口 |
可以从几个角度来对比。
| 对比点 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|
| 代理方式 | 实现接口 | 继承目标类 |
| 是否需要接口 | 需要 | 不需要 |
| 目标类是否可以是 final | 可以,因为代理的是接口 | 不可以,因为需要继承 |
| 目标方法是否可以是 final | 接口方法本身可被代理 | 不可以,因为 final 方法不能重写 |
| 私有方法能否代理 | 不能直接代理 | 不能代理 |
| 生成对象类型 | 接口的实现类 | 目标类的子类 |
| 调用方式 | 通过 InvocationHandler 拦截 | 通过 MethodInterceptor 拦截 |
| Spring 中的使用场景 | 目标类有接口时可用 | 目标类没有接口时可用 |
再用更直白的话说:
JDK 动态代理生成的是:
1 | 一个实现了接口的代理类 |
类似:
1 | class $Proxy0 implements UserService { |
CGLIB 生成的是:
1 | 一个继承目标类的子类 |
类似:
1 | class UserService$$EnhancerByCGLIB extends UserService { |
所以 JDK 动态代理更适合面向接口编程的场景。
CGLIB 更适合没有接口的普通类代理场景。
Spring AOP 和代理模式的关系
Spring AOP 的底层核心就是代理模式。
我们平时写业务代码时,可能只是这样写:
1 |
|
然后写一个切面:
1 |
|
表面上看,我们并没有写代理类。
但实际上,Spring 会在容器启动过程中判断哪些 Bean 需要被 AOP 增强。
如果某个 Bean 匹配了切点表达式,Spring 就会为它创建代理对象。
原本容器里应该放的是:
1 | UserServiceImpl |
但开启 AOP 后,容器里最终暴露给外部使用的可能是:
1 | UserServiceImpl 的代理对象 |
所以当 Controller 注入:
1 |
|
拿到的未必是原始的 UserServiceImpl,而可能是 Spring 生成的代理对象。
执行:
1 | userService.save(user); |
调用链大概是:
1 | Controller |
所以 Spring AOP 可以理解为:
1 | Spring 自动帮我们创建代理对象,并通过代理对象对目标方法进行增强。 |
Spring 到底选择 JDK 动态代理还是 CGLIB?
Spring AOP 默认会根据目标类情况选择代理方式。
通常可以这样理解:
1 | 如果目标类实现了接口,Spring 可以使用 JDK 动态代理。 |
比如:
1 | public interface UserService { |
1 |
|
这种情况有接口,Spring 可以使用 JDK 动态代理。
生成的代理对象实现的是 UserService 接口。
所以你一般应该这样注入:
1 |
|
而不是:
1 |
|
因为 JDK 动态代理生成的代理对象是接口实现类,不一定是 UserServiceImpl 的子类。
如果目标类没有接口:
1 |
|
这种情况下,JDK 动态代理没法使用,因为没有接口。
Spring 就会使用 CGLIB 创建一个子类代理对象。
生成的代理对象可以理解成:
1 | class UserService$$SpringCGLIB extends UserService { |
所以没有接口时,CGLIB 也能代理。
为什么 Spring 推荐面向接口编程?
因为接口可以把调用方和具体实现类解耦。
比如 Controller 依赖的是:
1 | private UserService userService; |
而不是:
1 | private UserServiceImpl userService; |
这样以后无论底层是真实对象、JDK 代理对象、CGLIB 代理对象,调用方都不用关心。
它只知道自己需要一个 UserService。
这也是代理模式非常重要的一个点:
1 | 代理对象和真实对象对外暴露相同的行为。 |
调用方不用知道自己拿到的到底是真实对象还是代理对象。
这就是“对调用方透明”。
Spring 事务为什么也依赖代理模式?
Spring 事务是代理模式最典型的应用之一。
平时我们写:
1 |
|
看起来只是加了一个 @Transactional 注解,事务就自动生效了。
但 Spring 并不是把事务代码直接塞进你的业务方法里面。
它的实现思想大概是:
1 | 调用方 |
如果用伪代码表示,事务代理大概像这样:
1 | public void createOrder(Order order) { |
这就是代理模式的典型应用。
真实业务方法只负责业务:
1 | orderMapper.insert(order); |
代理对象负责事务:
1 | 开启事务 |
这也是为什么有些场景下 @Transactional 会失效。
比如同一个类内部方法调用:
1 |
|
这里 create() 里面调用:
1 | this.saveOrder(); |
本质上是当前对象内部调用,没有经过 Spring 代理对象。
没有经过代理对象,就不会执行事务增强逻辑。
所以事务可能不会生效。
正确的理解应该是:
1 | @Transactional 不是方法自己变强了, |
如果调用没有经过代理对象,这一层事务逻辑自然就不会执行。
代理模式的本质总结
代理模式表面上是“多包了一层对象”,本质上是“控制方法调用过程”。
普通调用是:
1 | 调用方 -> 真实对象 |
代理调用是:
1 | 调用方 -> 代理对象 -> 真实对象 |
代理对象可以在这个过程中做很多事情:
1 | 方法调用前:权限校验、参数校验、开启事务、记录开始时间 |
所以代理模式适合处理这类问题:
1 | 某些逻辑和核心业务无关 |
比如:
1 | 日志 |
静态代理适合理解思想,但需要手写代理类,扩展性一般。
JDK 动态代理基于接口,要求目标类实现接口,代理对象本质上是接口的实现类。
CGLIB 动态代理基于继承,不要求接口,代理对象本质上是目标类的子类,但不能代理 final 类、final 方法和 private 方法。
Spring AOP 则是在这些动态代理技术之上,把代理对象的创建、增强逻辑的织入、目标方法的调用都封装好了。
所以我们只需要写:
1 |
|
或者:
1 |
|
Spring 就会在底层帮我们生成代理对象,并在合适的位置执行增强逻辑。
一句话总结:
1 | 代理模式不是为了替代真实对象,而是为了在不修改真实对象的前提下,控制和增强对真实对象的访问。 |
理解了代理模式,也就更容易理解 Spring AOP、Spring 事务、MyBatis Mapper 代理、RPC 框架中的远程代理等底层思想。




