【设计模式】代理模式
0. 引言
代理模式是非常常见的模式,在生活中的例子也非常多,例如你不好意思向你关系不太好朋友帮个忙,这时需要找一个和它关系好的应一个朋友帮忙转达,这个中间朋友就是代理对象。例如购买火车票不一定要去火车站买,可以通过12306网站或者去火车票代售点买。又如找女朋友、找保姆、找工作等都可以通过找中介完成。
1. 定义及特点
代理模式(Proxy)的定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。这种类型的设计模式属于结构型模式。
优点
- 功能增强:无需修改目标对象,即可添加额外功能。
- 访问控制:实现权限校验、懒加载等访问控制机制。
- 性能优化:通过缓存减少重复请求,提高系统性能。
缺点
- 系统复杂性:增加理解和维护成本。
- 性能开销:代理处理可能导致请求速度变慢。
- 调试难度:涉及多个对象,调试更复杂。
使用建议
- 根据具体需求选择合适的代理类型,如远程代理、虚拟代理、保护代理等。
- 确保代理类与真实对象接口一致,以便客户端透明地使用代理。
与其他模式的区别
- 与适配器模式的区别:适配器模式改变接口,而代理模式不改变接口。
- 与装饰器模式的区别:装饰器模式用于增强功能,代理模式用于控制访问。
2. 适用场景
代理模式的适用场景主要集中在需要对对象访问进行增强、控制或优化的各种情况。
代理模式主要有以下适用场景:
- 远程对象访问:当对象位于远程服务器时,客户端可以通过代理对象来间接访问这些远程对象,隐藏远程通信的复杂性。
- 延迟加载:对于重量级对象或需要较长时间才能创建的对象,可以使用代理模式来延迟其创建过程,直到真正需要时才进行实例化。
- 权限控制:通过代理对象,可以对目标对象的访问进行权限校验,确保只有具备相应权限的客户端才能访问。
- 性能优化:代理模式可以用于实现缓存机制,减少重复请求对目标对象的访问,从而提高系统性能。
- 日志记录和监控:代理对象可以在调用目标对象的方法之前和之后进行日志记录和监控,便于问题排查和性能分析。
- 事务管理:在数据库操作中,代理模式可以用于管理事务的开始、提交和回滚等操作,确保数据操作的一致性和完整性。
- 智能引用管理:代理模式可以用于实现智能引用,如引用计数、垃圾回收等,帮助管理内存和资源。
- 防火墙和安全代理:在网络安全领域,代理模式可以用于实现防火墙和安全代理,过滤和检查网络请求,确保系统的安全性。
- 接口适配:当需要将一个类的接口转换成客户端期望的另一个接口时,可以使用代理模式进行接口适配。
- 保护目标对象:代理模式可以用于保护目标对象免受恶意客户端的访问或破坏,通过代理对象对请求进行过滤和验证。
3. 代理模式的分类
通常我们都将代理分为 静态代理 和 动态代理 两大类。
静态代理 是在编译阶段就已经生成了代理类,代理类需要实现与目标对象相同的接口。
- 静态代理的优点是可以在不修改目标对象的前提下对目标对象的方法进行增强。
- 但其缺点是,需要为每个目标对象创建一个代理类,这会导致系统中类的数量增加,从而增加维护成本。
动态代理 是在程序运行时,通过反射机制动态生成的代理类。
- 动态代理的优点是可以为多个目标对象创建一个代理类,从而减少了类的数量,并且可以更加灵活地处理不同的情况。
- 但由于使用了反射机制,性能相对静态代理略低。
而动态代理又分为 JDK动态代理 和 CGLIB代理 两大类。
JDK动态代理:JDK动态代理是Java提供的一种动态代理实现方式,它利用 java.lang.reflect.Proxy
类和 InvocationHandler
接口来创建代理对象。JDK动态代理要求目标对象必须实现一个或多个接口,因此它只能代理实现了接口的类。
CGLIB代理:CGLIB代理是第三方库(如Spring AOP)常用的一种代理方式,它通过继承目标对象的方式来创建代理类。CGLIB代理的优点是不需要目标对象实现接口,可以代理任何类。但由于采用了继承的方式,目标类不能是 final
类,代理类也不能是 final
方法。
4. 代理模式的结构
代理模式的结构比较简单,主要是通过定义一个继承抽象主题的代理来包含真实主题,从而实现对真实主题的访问。
代理模式的主要角色如下:
- 抽象主题(Subject)类(业务接口类):通过接口或抽象类声明真实主题和代理对象实现的业务方法,服务端需要实现该方法。
- 真实主题(Real Subject)类(业务实现类):实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
代理模式的结构图:
在代码中,一般代理会被理解为代码增强,实际上就是在原代码逻辑前后增加一些代码逻辑,而 使调用者无感知。
5. 静态代理模式
PS: 为了能够更切合实际开发,接下来编写的【静态代理模式】代码将模拟实际业务开发,而不会根据上方的结构图进行编写。
5.1 代码的编写
静态代理服务于单个接口,我们来考虑实际工程中的一个例子,现在已经有业务代码实现一个增删功能,原有的业务代码由于仍有大量程序无法改变,现在新增需求,即以后每执行一个方法输出一个日志。
我们先直接看原有代码,包含一个接口和一个实现类:
1 | /** |
1 | /** |
现在我们准备一个代理类,为以上接口做增强:
1 | /** |
编写测试:
1 | /** |
运行代码后控制台打印:
1 | 成功添加! |
可以发现我们在并未修改原来业务代码的情况下成功对原有代码做了增强处理,这就是代理的好处!!
5.2 静态代理的问题
从上面的例子中我们不难看出,如果我们想要在不修改目标对象的前提下对目标对象的方法进行增强,只需要为它编写一个代理类即可。但是也会导致以下问题:
每当一个目标类需要被代理时,就需要为这个目标类编写一个代理类,这样会造成 代理类数量过多,不利于代码维护管理。
在上述的编码中,
UserServiceProxy
中的每个方法的核心功能后都模拟了日志的输出,当我们需要对这些输出日志进行修改时,需要对每个日志输出都行修改。很显然在静态代理模式下的 额外功能的维护性很差。
6. JDK 动态代理
为了解决静态代理中存在的问题,我们可以使用动态代理来解决,而在 JDK 中已经提供了方法来实现动态代理。
6.1 JDK 动态代理分析
在 JDK 中提供了 Proxy.newProxyInstance()
方法来实现动态代理,查看一下这个方法的参数信息:
1 | public static Object newProxyInstance(ClassLoader loader, |
Proxy.newProxyInstance()
方法的返回值就是为我们创建的代理对象,那这个方法的参数又代表什么含义呢?来看一下这三个参数:
loader
: 类加载器,用于加载代理类,使用真实对象的类加载器即可。interfaces
: 真实对象所实现的接口,代理模式真实对象和代理对象实现相同的接口。h
: 实现了InvocationHandler
接口的对象。
要实现动态代理的话,就必须需要实现 InvocationHandler
来自定义处理逻辑。那它具体是个啥呢?点开源码看一下:
1 | public interface InvocationHandler { |
invoke()
方法有下面三个参数:
- proxy:动态生成的代理类,也就是说
Proxy.newProxyInstance()
方法创建出的代理对象也会作为invoke()
方法的参数,我们一般不使用 proxy 参数。 - method:与代理类对象调用的方法相对应,比如我需要给
add()
方法添加额外功能,那么 method 就表示add()
方法。 - args:当前 method 方法的参数,比如如果
add()
方法有两个参数,那个 args[0] 就表示其第一个参数,args[1] 表示其第二个参数。
也就是说:通过 Proxy
类的 newProxyInstance()
创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler
接口的类的 invoke()
方法。 可以在 invoke()
方法中自定义处理逻辑,比如在方法执行前后做什么事情。
6.2 代码的编写
1 | /** |
运行上述代码后,控制台打印:
1 | --- Jdk before --- |
可以看到在原始方法的前后都成功进行增强。
7. CGLIB 动态代理
JDK 动态代理有一个最致命的问题是其 只能代理实现了接口的类。为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。
7.1 CGLIB 动态代理分析
CGLib 动态代理的编码和 JDK 动态代理的编码极其类似,只不过 CGLib 中是使用 Enhancer 对象来创建动态代理类。在 CGLIB 动态代理机制中 MethodInterceptor
接口和 Enhancer
类是核心。
创建 Enhancer 对象后需要对其三个属性进行赋值:
classLoader
:与 JDK 动态代理一样,也需要借用一个类加载器。superclass
:与 JDK 动态代理不一样,这里需要填入原始类的 Class 对象,表示代理类继承了原始类。callbacks
:自定义的额外功能。
针对 callbacks
属性的赋值,我们使用 setCallback()
方法进行赋值,传入 MethodInterceptor
对象即可。由于 MethodInterceptor 是一个函数式接口,因此接下来的编码中我将使用 Lambda 进行编写。
查看 MethodInterceptor
的源码:
1 | public interface MethodInterceptor extends Callback{ |
其中包含四个参数:
- obj : 被代理的对象(需要增强的对象)。
- method : 被拦截的方法(需要增强的方法)。
- args : 方法入参。
- proxy : 用于调用原始方法。
你可以通过 Enhancer
类来动态获取被代理类,当代理类调用方法的时候,实际调用的是 MethodInterceptor
中的 intercept
方法。
7.2 代码的编码
CGLIB是第三方提供的包,所以需要引入 jar 包的坐标:
1 | <!-- https://mvnrepository.com/artifact/cglib/cglib --> |
还是以前面代码为基础,直接编写 CGLIB 动态代理代码:
1 | /** |
控制台打印:
1 | --- cglib before --- |