【设计模式】代理模式
封面来源:代理设计模式 (refactoringguru.cn),如有侵权,请联系删除。
Java 设计模式学习网站:Java设计模式:23种设计模式全面解析(超级详细)
菜鸟教程:代理模式
Bilibili 视频:孙哥说Spring5 全部更新完毕 完整笔记、代码看置顶评论链接~学不会Spring? 因为你没找对人
1. 问题的引入
1.1 软件开发中的矛盾
在 JavaEE 分层开发中,我们一般分成三层,分别是 Controller 层、Service 层和 DAO 层,其中最为重要的是 Service 层。
那么 Service 层中包含了哪些代码?
在 Service 层中主要包含 核心功能(几十、上百行代码)和 额外功能(一小部分代码)。其中额外功能包括业务运算和 DAO 调用,这也是 Service 层中的重头戏,而额外功能不属于业务,甚至可有可无,因此代码量也很小,比如事务、日志、性能监控的代码都属于额外功能。
那么问题又来了,将 额外功能 写在 Service 层中好不好呢?
好不好的讨论得看站在哪个角度。
如果站在 Service 层的调用者(Controller 层)角度来看:就 Controller 层而言,无论 Service 层有没有其他的额外功能,事务的额外功能肯定是要有的,因此对于其调用者而言,额外功能的书写是必要的。
但如果站在软件设计者的角度来看:由于额外功能是可有可无的,当需要额外功能的时候,可以按照需求在 Service 层中编写相关代码,但是当额外功能不需要的时候,难道要到 Service 层中删除额外功能的代码吗?这显然是不行的,因为这样降低了代码的维护性,因此就软件设计者而言,额外功能写在 Service 层中是不好的。
在此,我们能发现一个矛盾:额外功能对 Controller 层来说是需要的,但对软件设计者来说在 Service 层中书写额外功能又是不好的。
那有没有什么办法能解决这个问题呢?
1.2 现实生活中的解决方案

上图是一个现实生活中很常见的场景:房客如果需要租房,那么就需要找房东租房。这个时候,房客相当于调用者,而房东相当于 Service。 房东这个 Service 中拥有一个名为 出租房屋 的 Method,在这个 Method 中核心功能是签合同和收钱,这点是毋庸置疑的,而额外功能是投放广告和带房客看房。
假设有一天房东不想干了,房东只想和房客签合同并收钱,不想在到处投放广告、带房客看房了,但这对房客来说现实吗?
对于房客这个调用者来说,额外功能是十分必要的。如果不投放广告,房客怎么知道房东有房源呢?房东不带房客看房,房客又怎么敢和房东签合同,房东又怎么收钱呢?
这个时候也出现了一个矛盾:额外功能对于调用者来说是必要的,但又不想把额外功能放在 Service 层中。

在现实生活中,为了解决这个矛盾就会引入另一个角色:中介。 此时房客不会直接向房东租房,而是向中介租房。 由中介进行投放广告和带房客看房等额外功能,当房客对房源满意后,也是和中介签订合同,只不过中介的出租房屋 Method 内部调用了房东出租房屋,因为中介并没有实际的房产证,是无法签订合同的,只是作为实际房东的代理。
那如果要更换额外功能呢?只需要更换一个中介即可,并不会在原有中介上进行“修改”。
注意: 中介(代理类)中的方法要和房东(目标类、原始类)中的方法签名一样(实现相同的接口),这样可以“迷惑”房客。新引入的中介(代理类)中既有额外功能,也调用了房东(目标类、原始类)的核心功能。
2. 模式的定义与特点
代理模式(Proxy):由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。这种类型的设计模式属于结构型模式。
在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。
优点
1、代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
2、代理对象可以扩展目标对象的功能;
3、代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性。
缺点
1、代理模式会造成系统设计中类的数量增加;
2、在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;
3、增加了系统的复杂度。
代理模式的分类
根据代理的创建时期,代理模式分为 静态代理 和 动态代理。
静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class 文件就已经存在了。
动态代理:在程序运行时,运用反射机制动态创建而成。
上述所列举的缺点也是针对 静态代理 而言的,动态代理 从一定程度上消除了以上缺点。
与其他模式的区别
1、与【适配器模式】的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
2、与【装饰器模式】的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
3. 模式的应用场景
当无法或不想直接引用某个对象或访问某个对象存在困难时,可以通过代理对象来间接访问。使用【代理模式】主要有两个目的:一是保护目标对象,二是增强目标对象。
【代理模式】主要有以下应用场景:
1、远程代理,这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。
2、虚拟代理,这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。
3、安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。
4、智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。
5、延迟加载,指为了提高系统的性能,延迟对目标的加载。例如,Hibernate 中就存在属性的延迟加载和关联表的延时加载。
4. 代理模式的结构
【代理模式】的主要角色如下:
1、抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
2、真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
3、代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
【代理模式】的结构图:
在代码中,一般代理会被理解为代码增强,实际上就是在原代码逻辑前后增加一些代码逻辑,而使调用者无感知,因此实际主题和代理都会实现相同的接口。
5. 静态代理模式
备注: 为了能够更切合实际开发,接下来编写的【静态代理模式】代码将模拟实际业务开发,而不会根据上方的结构图进行编写。
5.1 编码前的梳理
在编码之前,先声明几个名词的含义:
1、原始类(目标类):指的是实际业务类,其中包含核心功能(业务运算、DAO 调用等);
2、目标方法(原始方法):目标类(原始类)中方法就是目标方法;
3、额外功能(附加功能):如日志、事务、性能等。
最后梳理一个代理类的核心要素,这些要素是缺一不可的:
代理类 = 原始类(目标类)+ 额外功能 + 实现和原始类(目标类)相同的接口
5.2 静态代理的编码
静态代理: 需要程序员为每一个原始类(目标类)编写一个代理类,在程序运行前代理类的 .java 和 .class 文件就已经存在了。
一个实体类:
1 | /** |
再编写两个接口:
1 | /** |
1 | /** |
为这两个接口编写实现类:
1 | /** |
1 | /** |
然后为实现类编写代理类:
1 | /** |
1 | /** |
最后编写一个测试类测试一下:
1 | /** |
运行测试类后,控制台打印出:
--- log --- UserServiceImpl.register 业务运算 + DAO 调用 --- log --- UserServiceImpl.login ================================= --- log --- OrderServiceImpl.showOrder
代理成功!🎉
5.3 静态代理存在的问题
1、每当一个目标类需要被代理时,就需要为这个目标类编写一个代理类,这样会造成 代理类数量过多,不利于代码维护管理。
2、在上述的编码中,UserServiceProxy 和 OrderServiceProxy 中的每个方法的核心功能前都模拟了日志的输出,当我们需要对这些输出日志进行修改时,需要对每个日志输出都行修改。很显然在静态代理模式下的 额外功能的维护性很差。
6. JDK 动态代理
6.1 JDK 动态代理分析
为了解决静态代理中存在的问题,我们可以使用动态代理来解决,而在 JDK 中已经提供了方法来实现动态代理。
在 JDK 中提供了 Proxy.newProxyInstance()
方法来实现动态代理,查看一下这个方法的参数信息:
1 | public static Object newProxyInstance(ClassLoader loader, |
Proxy.newProxyInstance()
方法的返回值就是为我们创建的代理对象,那这个方法的参数又代表什么含义呢?
还记得我们前面说的代理类的三要素吗?
代理类 = 原始类(目标类)+ 额外功能 + 实现和原始类(目标类)相同的接口
原始类(目标类)很好办,有现成的代码无需担心;代理类需要实现和原始类(目标类)相同的接口,这也很好办,很显然 Proxy.newProxyInstance()
的第二个参数 interfaces
就是用来指定实现的接口的,通过原始对象的 getClass().getInterfaces()
方法即可获得相同的接口;那额外功能呢?
额外功能就需要用到 Proxy.newProxyInstance()
的第三个参数:InvocationHandler
。
InvocationHandler
又是个啥呢?点进源码看看:
1 | public interface InvocationHandler { |
InvocationHandler
是一个函数式接口,其内部有且仅有一个方法。
针对 InvocationHandler
中的 invoke()
方法,我们需要知道:
① invoke()
方法的作用;
② invoke()
方法参数的作用;
③ invoke()
方法返回什么样的返回值。
invoke()
方法的作用很简单,就是用来书写额外功能的,使额外功能运行在原始方法之前、之后、前后乃至抛出异常。
invoke()
方法的返回值就是代理方法的返回值。
invoke()
方法有三个参数,分别是:proxy、method 和 args。其中 proxy 表示代理对象,也就是说 Proxy.newProxyInstance()
方法创建出的代理对象也会作为 invoke()
方法的参数,我们一般不使用 proxy 参数;第二个参数 method 表示需要被添加额外功能的原始方法,比如我需要给 login()
方法添加额外功能,那么 method 就表示 login()
方法;第三个参数 args 表示需要被添加额外功能的原始方法的参数列表,比如 login()
方法有两个参数,那个 args[0] 就表示其第一个参数,args[1] 表示其第二个参数。
通过对 invoke()
方法的三个参数,我们可以拿到原始方法以及原始方法所需要的参数,这下就直接可以调用原始方法了?这显然是不行了,我们还需要原始对象。因此要想调用原始方法,我们可以:
1 | Object ret = method.invoke(原始对象, args); |
其中,原始对象可以通过原始类 new
出来,ret
就是原始方法的返回值。method.invoke()
是通过反射来调用方法的,它跟下述两个方式调用方法是一样的,只不过这里的 method
既可以指 login()
也可以指 register()
,通过反射来调用方法更加灵活。
1 | // Object ret = method.invoke(原始对象, args); 等价于 |
Proxy.newProxyInstance()
方法的后两个参数的作用我们已经知道了,那第一个参数 ClassLoader
又有什么用?或者说怎么用呢?
ClassLoader 翻译一下就是 类加载器,类加载器有什么作用呢?类加载器有两个作用:
1、类加载器可以把对应类的字节码文件加载进 JVM 中;
2、创建对象时,需要通过类加载器创建类的 Class 对象,进而创建这个类的对象。比方说,当我们需要创建 User 类的 user 对象时,需要先创建 User 类的 Class 对象,然后才能通过 new User()
的方式创建 user 对象。
再来分析一下 类加载器的运行过程:
假定我现在需要创建 user 对象,那么我需要先编码完成 User.java 文件,然后通过编译得到 User.class 文件,User.class 文件中存放着 User 类的字节码信息。那么得到 User.class 后可以创建对象了吗?
这显然是不行的,因为我们知道创建对象和运行对象是需要在 Java 虚拟机(JVM)中的。那么我们就需要把字节码文件加载进 JVM 中,而这一步就是通过类加载器来完成的。还没完,还需要创建 User 类的 Class 对象才行,而这一步也是通过类加载器来完成的,得到 Class 对象后就可以创建 user 对象了。

很明显,类加载器 在创建对象的过程中是十分重要的,那怎么获得类加载器呢?无需担心,虚拟机会默认为每一个类的 .class 文件自动分配与之对应的类加载器。
那为什么在 JDK 动态代理的 Proxy.newProxyInstance()
方法的第一个参数就需要传递一个类加载器呢?虚拟机不是会默认提供吗?
JDK 动态代理也是在虚拟机中获得动态代理类,进而创建代理对象。 那么问题来了,动态代理类有源文件吗?很显然是没有的,动态代理与静态代理的一个区别就是解决了静态代理代理类过多的缺点。既然动态代理类没有源文件,那就没有 .class 文件了。没有 .class 文件,那又是如何获取动态代理类的字节码来创建对象的呢?
动态代理就是通过动态字节码技术来创建字节码并直接写入虚拟机中,也就是说没有字节码加载进 JVM 的过程。使用 Proxy.newProxyInstance()
和这个方法的后两个参数就可以得到动态代理类的字节码并写入虚拟机中。通过前面的分析我们知道,仅仅拥有动态代理类的字节码仍然是不能创建代理对象的,还需要创建 Class 对象才行。创建 Class 对象时需要类加载器的介入,但动态代理类没有与之对应的 .class 文件,也就没有与之对应的类加载器,这咋办?
既然动态代理类没有与之对应的类加载器,那我们就可以 借一个类加载器来使用,通过借来的类加载器来创建 Class 对象进而创建动态代理对象,这也是 Proxy.newProxyInstance()
方法的第一个参数就需要传入一个类加载器的原因。
那么借谁的类加载器呢?随便借一个就行,谁有源文件,谁就有 .class 文件及其与之对应的类加载器。比如就可以借用原始类的类加载器。
6.2 JDK 动态代理的编码
1 | /** |
运行上述代码后,控制台打印出:
--- start --- UserServiceImpl.login --- end --- --- start --- UserServiceImpl.register 业务运算 + DAO 调用 --- end ---
证明我们编写的 JDK 动态代理是没有问题的。
上述代码中,Proxy.newProxyInstance()
方法的 第一个参数 使用的是 Thread.currentThread().getContextClassLoader()
,当然也可以是其他的,比如 UserService.class.getClassLoader()
,或者 JdkProxy.class.getClassLoader()
,这些都是没有问题的。
7. CGLib 动态代理
7.1 与 JDK 动态代理的区别
CGLib 动态代理和 JDK 动态代理的目标是一致的,都是为了规避静态代理的缺点并创建出代理对象。那它们有什么区别呢?
在 JDK 动态代理中,我们需要 保证动态代理类和原始类实现相同的接口,其原因如下:
1、保证代理类和原始类的方法一致,用于“迷惑”调用者;
2、代理类可以提供新的实现,即能对原始方法进行拓展,添加额外功能。
但在实际开发过程中很有可能会出现这样一个场景:原始类没有实现任何接口。那怎么为这样的类创建代理类呢?显然使用 JDK 动态代理是不行的了。
这里插一句: 无论原始类是否实现了接口,代理类和原始类要具有共同的方法。
在这种情况下,就可以使用 CGLib 动态代理来解决。
为了满足代理类和原始类具有共同的方法,使用 CGLib 动态代理时,要求 代理类继承原始类,因此原始类不能被 final
修饰。
简单来说:JDK 动态代理中,原始类和代理类是“兄弟”关系;在 CGLib 动态代理中,原始类和代理类是父子关系。
7.2 CGLib 动态代理的编码
依赖的导入
需要先引入 CGLib 的依赖:
1 | <!-- https://mvnrepository.com/artifact/cglib/cglib --> |
原始类的准备
另外创建一个实体类:
1 | /** |
创建一个原始类:
1 | /** |
编码前的分析
CGLib 动态代理的编码和 JDK 动态代理的编码极其类似,只不过 CGLib 中是使用 Enhancer
对象来创建动态代理类。
创建 Enhancer
对象后需要对其三个属性进行赋值:
1、classLoader
:与 JDK 动态代理一样,也需要借用一个类加载器;
2、superclass
:与 JDK 动态代理不一样,这里需要填入原始类的 Class 对象,表示代理类继承了原始类;
3、callbacks
:自定义的额外功能。
针对 callbacks
属性的赋值,我们使用 setCallback()
方法进行赋值,传入 MethodInterceptor
对象即可。由于 MethodInterceptor
是一个函数式接口,因此接下来的编码中我将使用 Lambda 进行编写。
查看 MethodInterceptor
的源码:
1 | public interface MethodInterceptor extends Callback{ |
与 JDK 动态代理类似,intercept()
方法的返回值为代理方法的返回值。其参数含义如下:
1、obj 表示由 CGLib 生成的代理类实例(代理对象);
2、method 表示需要被添加额外功能的原始方法;
3、args 表示参数列表;
4、proxy 表示 CGLib 生成的代理对象中的代理方法,可以根据需要多次执行原始方法。
查看 MethodProxy
的源码,其中的注释是这样的:
1 | /** |
也就是说,这个类可以用来执行原始方法,也可以用来在同一个类中不同的对象中调用相同的方法。
使用 method 可以通过反射来获取运行时的对象的方法,使用 methodProxy 可以创建一个反射代理类,通过代理类来调用方法。
根据 MethodInterceptor#intercept()
方法的注释可知:为代理类生成的所有代理方法都会调用 intercept()
方法,而不是原始方法。原始方法可以使用 Method
对象通过反射来调用,也可以使用 MethodProxy
对象来调用,而且使用 MethodProxy
的效率更高。
因此在 CGLib 中,调用原始方法有三种方式:
1 | Object ret = method.invoke(studentService, params); |
CGLib 动态代理编码
1 | /** |
运行上述代码后,控制台打印出:
--- cglib start --- StudentService.create --- cglib end --- --- cglib start --- StudentService.delete --- cglib end ---
证明我们编写的 CGLib 动态代理是没有问题的。
7.3 创建抽象类的代理对象
如果一个抽象类没有任何子类,能否为其创建代理对象呢?
答案是可行的,使用 CGLib 即可。
现有这样一个原始抽象类:
1 | /** |
然后使用 CGLib 来创建代理对象:
1 |
|
运行上述代码后,控制台打印出:
--- Abstract CGLib --- AbstractStudentService.update
证明我们使用 CGLib 为抽象类创建代理对象是没有问题的。
7.4 Callback 的种类
前文中使用 Enhancer
创建代理对象时,都会有 enhancer.setCallback()
这一步,要求传入一个 Callback
对象,前文中都是使用的是 MethodInterceptor
,但 Callback
的种类并不是只有这一种。
本节中需要使用到的原始类:
1 |
|
NoOp
这个回调很简单,就是什么都不干。
1 |
|
Hello
LazyLoader
用于实现懒加载的回调,当代理对象的方法 首次 被调用时,就会触发回调,之后每次对方法的调用都是第一次懒加载返回的 Bean 的方法的调用。
1 |
|
build prepare loading after loading mofan mofan
Dispatcher
Dispatcher
和 LazyLoader
的作用很相似,只不过每次对代理对象的方法的调用时都会触发回调。
1 |
|
build prepare loading after loading mofan prepare loading after loading mofan
InvocationHandler
CGLib 的 InvocationHandler
和 JDK 的 InvocationHandler
很类似,只不过如果对参数中的 method
再次进行调用时,会重复触发回调。如果要对原始方法进行调用,还是使用 MethodInterceptor
更好。
1 |
|
hello world
FixedValue
该回调常用于替换原始方法的返回值为回调方法的返回值,但必须保证返回类型是兼容的,否则会抛出异常。
1 |
|
7.5 CallbackFilter
一种 Callback
只能对原始类进行某种增强,如果需要一些定制化操作,比如满足不同的条件进行不同的增强,是无法利用 Callback
实现的,这时 CallbackFilter
就排上用场了。
当通过 CallbackFilter
对原始类进行增强后,原始类中的方法会根据设置的 Filter 与某个特定的 Callback
进行映射,以完成定制化需求。
基本使用
1 |
|
在 setCallbackFilter()
时,根据不同的方法名称返回了不同的 int
类型数据,该数据被代理类的各个方法在回调数组 Callback[]
(即 setCallbacks()
时传入的数组)中的位置索引。
比如,当方法名称为 sayHello
时,返回 0
,那么就会回调 firstCallback
。
上述测试方法运行后有:
before getMyName1 Hello after getMyName1 before getMyName2 Hello World after getMyName2
CallbackHelper
CGLib 提供了 CallbackHelper
类来简化 CallbackFilter
和 Callback[]
的设置与映射。
创建 CallbackHelper
实例时,需要传入原始类的 Class
和原始类父接口的 Class
数组,根据这两者获取到 Method
集合,然后遍历该集合,调用重写的 getCallback()
方法,获取到每个 Method
对应的 Callback
以建立 Method
与 Callback
的映射。
1 |
|
before Hello after