设计模式之代理模式
代理模式概览
代理模式是一种结构型模式,其特点就是 使用代理对象来代替对真实对象的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
代理模式总结一句话就是:通过一个中间人 (代理) 去访问真正的对象。
代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。
代理模式的最经典直接的生活化案例就是 代购。
假设你想买一款只在国外售卖的商品 (比如某款限量版球鞋或者化妆品),但你本人没法亲自去国外购买。于是你找了一个 代购,他帮你去国外购买并寄回给你。
在这个过程中,主要分为以下几个角色:
- 真实对象:你自己 (真正想买商品的人)
- 代理对象 (proxy):代购的人
- 目标:代购人代替你去买你需要的商品,你自己没有必要亲自去买
代购并不改变你最终要买的东西,而是 代替你执行购买操作,并且在过程中可能还附加了 额外服务 (比如验货、打包、国际邮寄等)。在程序中我们通常把这种 “额外服务” 叫做 “功能增强”,这也是代理模式的典型应用:扩展目标对象的功能。
再来看一个买电影票的案例:你想看一部电影,但并不是直接去电影院柜台买票,而是通过 猫眼、淘票票、电影院官网等平台 买票。这些平台就是电影票的代理。
在这个过程中,主要分为以下几个角色:
- 真实对象:电影院售票系统 (真正出售电影票的系统)
- 代理对象:第三方购票平台 (如猫眼、淘票票)
- 目标:你通过代理平台买票,平台再与电影院系统交互
代理平台并没有真正 “放电影”,它只是 代替用户去访问影院系统,完成订票流程,有时还提供额外功能 (如选座、优惠、评价等)。
以上两个案例就是代理模式最典型的使用了,而代理模式也是有分类的,具体如下图:

静态代理
静态代理中,我们对目标对象的每个方法的增强都是 手动完成的,非常不灵活 (比如接口一旦新增加方法,目标对象和代理对象都要进行修改) 且麻烦 (需要对每个目标类都单独写一个代理类)。实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。
上面我们是从实现和应用角度来说的静态代理,从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
静态代理实现步骤:
-
定义一个接口及其实现类;
-
创建一个代理类同样实现这个接口
-
将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。
这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
下面是一个静态代理的简单实现。
1、先定义接口
1 | /** |
2、再定义接口的实现 (目标对象、被代理的类)
1 | /** |
3、定义代理类,执行代理 (调用目标方法并可对原始功能进行扩展)
1 | /** |
4、客户端使用
1 | public class Client { |
执行之后控制台输出的内容如下:
1 | 发送短信前做一些事情 ... |
通过以上的案例我们就已经实现了代理类对原始对象中 sendMessage()
方法的增强,这就是静态代理。
动态代理
相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类。从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
动态代理技术的最最典型的应该应该就是 Spring AOP 和 RPC 框架了,它们的实现都依赖了动态代理。动态代理在我们日常开发中使用得相对较少,但是在框架中几乎是必用的一门技术,学会了之后可以有助于我们在源码阅读时更容易理解。
而对于 Java 来说,动态代理的实现方式有很多种,比如 JDK 动态代理、CGLIB 动态代理 等等,这两种代理方式会注重讲解,并且它们之间的区别也会做相应说明。
JDK 动态代理
实现 JDK 动态代理
在 JDK 的动态代理中,存在两个核心:一个是 Proxy 类,位于 java.lang.reflect.Proxy
下,其中最核心的方法就是 newProxyInstance()
,其作用就是用来创建代理对象的,方法签名如下:
1 | /** |
第二个核心就是 InvocationHandler 接口,其中最重要的方法就是 invoke()
方法,用于处理代理方法的逻辑 (调用原始方法并提供功能增强)。同时该接口也是 newProxyInstance()
的第三个参数,因此我们可以直接 new 这个接口使用匿名内部类实现或者使用 Lambda 表达式简化又或者直接写一个类实现这个接口,传递参数时创建该类的对象。
invoke()
方法的方法签名如下:
1 | /** |
你通过
Proxy
类的newProxyInstance()
创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler
接口的类的invoke()
方法。 你可以在invoke()
方法中自定义处理逻辑,比如在方法执行前后做一些事情。
创建 JDK 动态代理的步骤是这样的:
- 定义一个接口及其接口的实现
- 定义一个类实现
InvocationHandler
接口并重写invoke()
方法,我们在该方法中可以自定义处理逻辑并调用原始方法,这样就可以实现调用前后的功能增强 - 通过
newProxyInstance()
方法创建代理对象,将实现InvocationHandler
接口的类作为第三个参数传入。
下面是 JDK 动态代理的一个简单案例。
1、定义接口
1 | /** |
2、实现接口 (目标对象、被代理的对象)
1 | /** |
3、定义 JDK 动态代理类,处理逻辑并进行增强
1 | /** |
当我们的动态代理对象调用原生方法的时候,最终实际上调用到的是 invoke()
方法,然后 invoke()
方法代替我们去调用了被代理对象的原生方法。
4、获取代理对象
1 | /** |
其中第二个参数 new Class[]{Service.class}
也可以写为 target.getClass().getInterfaces()
。
5、实际使用
1 | public class Client { |
执行之后控制台输出的内容如下:
1 | 可以在调用目标方法之前做一些事情 ... |
JDK 动态代理的特性
JDK 动态代理是只能够 代理接口 的,并且是通过反射的机制在运行时动态生成代理类的
CGLIB 动态代理
实现 CGLIB 动态代理
为了避免 JDK 动态代理只能代理接口的缺点,我们可以使用 CGLIB 动态代理技术。CGLIB (Code Generation Library) 是一个基于 ASM 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。很多知名的开源框架都使用到了 CGLIB, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
CGLIB 动态代理中也存在两个核心。第一个核心就是 MethodInterceptor
接口中的 intercept()
方法。作用就是拦截需要代理的方法,进行自定义逻辑或增强,其方法签名如下:
1 | public interface MethodInterceptor extends Callback { |
第二个比较核心的就是 Enhancer
类。这是代理类,我们需要创建 Enhancer
对象,并设置类加载器、被代理类、方法回调 (方法增强) 等参数完成代理,最后使用 Enhancer.create()
创建代理类
创建 CGLIB 动态代理类的基本步骤是:
-
先引入 CGLIB 的依赖
-
定义一个类
-
自定义
MethodInterceptor
并重写intercept()
方法,intercept()
用于拦截增强被代理类的方法,和 JDK 动态代理中的invoke()
方法类似 -
通过
Enhancer
类的create()
创建代理类;
下面是一个 CGLIB 动态代理的简单案例。
1、引入 CGLIB 依赖
1 | <!-- Cglib --> |
2、创建被代理类
1 | /** |
3、定义方法拦截器 (实现 MethodInterceptor
并重写 intercept()
方法)
1 | /** |
4、新建代理类,实现代理
1 | /** |
5、实际调用
1 | public class Client { |
执行之后控制台输出的内容如下:
1 | 在方法之前做一些事情 ... |
CGLIB 动态代理特性
CGLIB 是通过生成被代理类的子类来实现动态代理的,是需要有继承关系的。因此被 final
修饰的类或方法是不能被代理的;private
修饰的方法也是不能被代理的,因为私有方法不能被继承。
JDK 和 CGLIB 动态代理对比
- JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。
- CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为
final
类型的类和方法,private
方法也无法代理。
就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
静态代理和动态代理对比
灵活性:静态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。但是在静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的。
JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。