【设计模式】代理模式(Proxy Pattern)
创始人
2024-03-28 08:39:11
0

代理模式属于结构型模式,当一个对象不处于相同内存空间时、创建开销大时、需要进行安全控制时或需要代理处理一些其他事物时可以使用代理模式。代理模式通过为另一个类提供一个替身类来控制对这个类的对象的访问。


文章目录

  • 代理模式的介绍
    • 代理的分类:
    • 优点
    • 缺点
    • 应用场景
    • 注意事项
  • 静态代理模式的使用
    • 类图
      • 三种角色
    • 静态代理实现方法
      • 第一步,编写抽象类
        • 租房接口类
      • 第二步,编写真实对象类
        • 房东类
      • 第三步,编写代理类
        • 房屋中介代理类
      • 第四步,编写客户程序测试
        • 客户程序
        • 测试结果
  • 动态代理模式的使用
    • 动态代理的介绍
    • 通过JDK的`Proxy`类实现动态代理
      • 实现方法
        • 第一步,创建代理实现类
          • MyProxy
        • 第一步,客户程序
          • Main
          • 测试结果
    • 通过CGLIB实现动态代理
      • 添加 jar 包
      • 实现方法
        • 第一步,创建自定义代理类
          • MyCglibProxy
        • 第二步,编写客户程序测试
          • Main
          • 测试结果
  • 本文参考


代理模式的介绍

代理模式(Proxy Pattern)是最常用的设计模式之一,其根本意义是当客户程序不方便直接访问一个对象时,使用一个代理对象间接的控制访问该对象。例如当我需要调用A类时,不直接实例化A类而是实例化它的代理对象B类(它们二者应当继承于同一个父类),在B类中持有A类的实例化对象,以此控制B类间接调用A类。


代理的分类:

  • 远程代理:对于在不同内存空间运行的远程对象,可以使用一个代理对象来远程代理它;

  • 虚拟代理:创建开销很大的对象时,可以先使用代理对象先代替它(占位符),当真实对象创建后,代理对象再将请求委托给它;

  • 安全代理:通过代理对像限制真实对象的访问权限

  • 智能指引:在访问真实对象时使用代理对象执行一些附加的操作,如访问页面时添加点击数;

  • 缓存代理:在客户程序与真实对象间添加一层缓存,用于加速对真实对象的访问速度,例如数据库的连接就存在缓存。

优点

  • 可以在客户端无感知的情况下,对访问对象增加缓存、权限限制等额外功能

  • 具有高扩展性,可以在不修改客户程序和被代理对象的前提下,增加新的代理类

缺点

  • 真实对象与客户程序间多了一层代理对象,可能导致请求处理速度变慢

  • 增加了系统的复杂性

应用场景

  • 在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、日志
  • Spring AOP 底层的实现原理就是基于动态代理
  • 接口请求的缓存功能,对于某些接口请求,如果入参相同,在设定的过期时间内,直接返回缓存结果,而不用重新进行逻辑处理

注意事项

  • 和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制



静态代理模式的使用

举例:租客需要租房,但觉得太麻烦,于是就通过房租中介来租房。

类图

image-20221207204845742

三种角色

  • 抽象主题角色(Subject):声明真实主题角色与代理角色的共同接口方法
  • 真实主题角色(Real Subject):负责执行具体的任务,客户程序可以通过代理角色间接的调用真实主题角色的方法
  • 代理角色(Proxy):持有对真实主题角色的引用,负责调用真实主题角色中相应的接口方法



静态代理实现方法

第一步,编写抽象类

租房接口类

package 设计模式.结构型模式.代理模式.静态代理;/*** **抽象主题角色(Subject)**:声明真实主题角色与代理角色的共同接口方法*/
public interface 租房接口类 {void 租();
}

第二步,编写真实对象类

房东类

package 设计模式.结构型模式.代理模式.静态代理;/*** **真实主题角色(Real Subject)**:负责执行具体的任务,客户程序可以通过代理角色间接的调用真实主题角色的方法*/
public class 房东类 implements 租房接口类 {@Overridepublic void 租() {System.out.println("实现租房");}
}

第三步,编写代理类

房屋中介代理类

package 设计模式.结构型模式.代理模式.静态代理;/*** **代理角色(Proxy)**:持有对真实主题角色的引用,负责调用真实主题角色中相应的接口方法*/
public class 房屋中介代理类 implements 租房接口类 {租房接口类 房东; // 持有对真实对象的引用public 房屋中介代理类() {房东 = new 房东类(); // 创建真实对象}@Overridepublic void 租() {System.out.println("中介找房!");房东.租();}
}

第四步,编写客户程序测试

客户程序

package 设计模式.结构型模式.代理模式;import 设计模式.结构型模式.代理模式.静态代理.房东类;
import 设计模式.结构型模式.代理模式.静态代理.房屋中介代理类;
import 设计模式.结构型模式.代理模式.静态代理.租房接口类;public class 客户程序 {public static void main(String[] args) {// 使用代理模式租房接口类 找房 = new 房屋中介代理类();找房.租();// 不使用代理System.out.println("————————————");租房接口类 房东 = new 房东类();房东.租();}
}

测试结果

中介找房!
实现租房
————————————
实现租房Process finished with exit code 0

image-20221207205709719




动态代理模式的使用

在真实环境下,静态代理使用得很少,因为真实类的方法很多时,静态代理类的代码量会很大,并且每个真实类都需要一个单独的代理类,所以此时可以使用动态代理

动态代理的介绍

  • 动态代理允许只有一个方法的单个类(代理类)为任意个真实类任意个方法的调用提供服务

  • 动态代理使用Java的反射机制实现,在程序运行期间由JVM动态生成代理类,此时代理类和真实类不用实现同一个接口

  • 但使用反射难免会降低执行效率

  • 动态代理的实现方法有两种

    • 通过JDK的Proxy类实现动态代理
    • 通过CGLIB实现动态代理

通过JDK的Proxy类实现动态代理

此方法实际上代理的是接口,所以要求代理的类必需实现一个接口,并且因为是实现接口,代理对象就只能使用接口的方法。下面我将对HashMapArrayList进行代理,以此控制添加元素时合法性。

实现方法


第一步,创建代理实现类

MyProxy
package 设计模式.结构型模式.代理模式.动态代理.jdk;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;/*** JDK的Proxy方式代理* 自定义的代理类,其中必须维护一个真实对象,并且实现InvocationHandler接口* 此代理对象其实是代理接口,通过动态实现代理接口来实现对真实对象的代理*/
public class MyProxy implements InvocationHandler {private Object bean;public MyProxy(Object bean) {// 维护一个真实对象this.bean = bean;}/*** InvocationHandler的实现方法,代理的核心方法,所有代理对象的请求从这个方法流转到真实对象* @param proxy 当前的代理对象* @param method 当前需要执行的方法* @param args 方法参数列表* @return 对接真实对象中方法的返回类型* @throws Throwable*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 同时代理Map的put方法和List的add方法if (method.getName().equals("put") || method.getName().equals("add")){String value = null;// 如果方法名为putif (method.getName().equals("put")){System.out.println("map存入的键:"+args[0]+"\t 值:"+args[1]);value = (String) args[1];}else { // 不为put肯定为addSystem.out.println("list存入的值:"+args[0]);value = (String) args[0];}// List的add方法的返回值是布尔型,所以返回false而不能返回null,而Map的put方法返回值不明确,所以仅考虑add方法if (!filter(value)) return false;}// 调用真实对象的指定方法并返回值 bean:指定真实对象(千万记得是真实对象,填proxy会死循环) args:传参列表return method.invoke(bean, args);}/*** 过滤掉黄色信息* @param value 原信息* @return false 不能通行信息 true 可通行信息*/boolean filter(String value){if (value.equals("黄色")){System.out.println("好啊!搞黄色,不能存不能存!");return false;}else{System.out.println("很好,没有搞黄色,允许存入");}return true;}
}

第一步,客户程序

Main
package 设计模式.结构型模式.代理模式.动态代理.jdk;import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;public class Main {/*** static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 新建一个代理对象* loader : 类加载器,一般默认用代理类的类加载器,为null也可以* inerfaces: 代理对象需要实现的接口类* InvocationHandler: 代理方法的实现类** 使用Proxy代理要求代理类必须实现某个接口,Proxy会动态创建实现这个接口的代理类,所以也只能使用接口定义的方法* 就和静态代理的结构一样,只不过现在不需要手动创建代理类而是动态生成* 此示例中的HashMap实现的Map接口,ArrayList实现的List的接口,他们的代理对象也是实现这两个接口*/public static void main(String[] args) {// 创建代理对象,代理对象中必须维护一个真实对象,值都存在真实对象上MyProxy myProxy = new MyProxy(new HashMap());Map map = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class}, myProxy);System.out.println("————存储Map数据————");map.put("张三","这是正经信息");map.put("李四","黄色");System.out.println("————读取Map数据————");System.out.println("张三的信息:"+map.get("张三"));System.out.println("李四的信息:"+map.get("李四"));System.out.println("\n");// 新建代理对象,其中必须维护一个真实对象,值都存在真实对象上myProxy = new MyProxy(new ArrayList());List list = (List) Proxy.newProxyInstance(myProxy.getClass().getClassLoader(),new Class[]{List.class},myProxy);System.out.println("————存储List数据————");list.add("这是正经信息");list.add("黄色");System.out.println("————读取List数据————");list.forEach(System.out::println);}
}
测试结果
————存储Map数据————
map存入的键:张三	 值:这是正经信息
很好,没有搞黄色,允许存入
map存入的键:李四	 值:黄色
好啊!搞黄色,不能存不能存!
————读取Map数据————
张三的信息:这是正经信息
李四的信息:null————存储List数据————
list存入的值:这是正经信息
很好,没有搞黄色,允许存入
list存入的值:黄色
好啊!搞黄色,不能存不能存!
————读取List数据————
这是正经信息Process finished with exit code 0

img-V3hoIjy2-1670513095568



通过CGLIB实现动态代理

CGLIB(Code Generation Library)是一个高性能开源的代码生成包,它被许多框架所使用,其底层是通过使用一个小而快的字节码处理框架 ASM(Java 字节码操控框架)转换字节码并生成新的类。因此 CGLIB 要依赖于 ASM 的包。

JDK 的动态代理机制只能代理实现了接口的类,而对于没有实现接口的类就不能使用JDK 的 Proxy 类生成代理对象,cglib 是针对类来实现代理的,他的原理是对指定的目标类生成一个子类并通过回调的方式来实现增强,但因为采用的是继承,所以不能对 final 修饰的类进行代理

添加 jar 包

  • cglib.jarasm.jar

  • 如果使用cglib-nodep.jar则不需要添加asm.jar,因为包内部包含asm的类

  • CGLIB的下载(github)

实现方法


第一步,创建自定义代理类

MyCglibProxy
package 设计模式.结构型模式.代理模式.动态代理.cglib;import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;/*** CGLIB动态代理* 通过动态创建一个被代理对象的子类来实现代理,此方法并不需要被代理类实现接口,所以可以用于普通对象的代理*/
public class MyCglibProxy implements MethodInterceptor {private Enhancer enhancer; // 增强类,代理类private Object bean; // 真实对象,其实通过invokeSuper方法调用父类方法就行,没必要维护一个真实对象public MyCglibProxy(Object bean) {// 维护一个真实对象this.bean = bean;// 代理对象enhancer = new Enhancer();}/*** 获取代理对象* @return*/public Object getProxy(){// 设置父类,也就是要代理的类enhancer.setSuperclass(bean.getClass());// 设置回调方法,也就是代理类需要执行的方法enhancer.setCallback(this);// 创建一个代理对象return enhancer.create();}/*** 代理转发,所有代理对象的请求从这个方法流转到真实对象* @param o 当前代理对象* @param method 当前方法* @param objects 方法参数列表* @param methodProxy 方法代理,通过方法代理调用方法可以提供执行速度* @return* @throws Throwable*/@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {// 同时代理Map的put方法和List的add方法if (method.getName().equals("put") || method.getName().equals("add")){String value = null;if (method.getName().equals("put")){System.out.println("map存入的键:"+objects[0]+"\t 值:"+objects[1]);value = (String) objects[1];}else {System.out.println("list存入的值:"+objects[0]);value = (String) objects[0];}// List的add方法的返回值是布尔型,所以返回false而不能返回null,而Map的put方法返回值不明确,所以仅考虑add方法if (!filter(value)) return false;}// jdk的代理调用方式,调用速度比后两者慢,不推荐//method.invoke(bean,objects);// 调用真实对象的指定方法,但使用FastClass提高了调用效率//methodProxy.invoke(bean,objects);// 调用当前代理对象的父类(也就是被代理对象)的方法,这样做可以不用维护真实对象(这里不能填真实对象,否则会报异常)//methodProxy.invokeSuper(o,objects);return methodProxy.invokeSuper(o, objects);}/*** 过滤掉黄色信息* @param value 原信息* @return false 不能通行信息 true 可通行信息*/boolean filter(String value){if (value.equals("黄色")){System.out.println("好啊!搞黄色,不能存不能存!");return false;}else{System.out.println("很好,没有搞黄色,允许存入");}return true;}}

第二步,编写客户程序测试

Main
package 设计模式.结构型模式.代理模式.动态代理.cglib;import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;public class Main {public static void main(String[] args) {// 创建自定义代理对象MyCglibProxy myCglibProxy = new MyCglibProxy(new HashMap());// 获取代理对象HashMap map = (HashMap) myCglibProxy.getProxy();System.out.println("————存储Map数据————");map.put("张三","这是正经信息");map.put("李四","黄色");System.out.println("————读取Map数据————");System.out.println("张三的信息:"+map.get("张三"));System.out.println("李四的信息:"+map.get("李四"));System.out.println("\n");// 新建自定义代理对象myCglibProxy = new MyCglibProxy(new ArrayList());// 获取代理对象ArrayList list = (ArrayList) myCglibProxy.getProxy();System.out.println("————存储List数据————");list.add("这是正经信息");list.add("黄色");System.out.println("————读取List数据————");list.forEach(System.out::println);}
}
测试结果

同JDK方法一样

————存储Map数据————
map存入的键:张三	 值:这是正经信息
很好,没有搞黄色,允许存入
map存入的键:李四	 值:黄色
好啊!搞黄色,不能存不能存!
————读取Map数据————
张三的信息:这是正经信息
李四的信息:null————存储List数据————
list存入的值:这是正经信息
很好,没有搞黄色,允许存入
list存入的值:黄色
好啊!搞黄色,不能存不能存!
————读取List数据————
这是正经信息Process finished with exit code 0



本文参考

一文搞懂代理模式-CSDN

代理模式详细讲解-CSDN

代理模式是个什么模式?-知乎

CGLIB动态代理之intercept函数刨析-CSDN

浅谈Java代理:JDK动态代理-Proxy.newProxyInstance-CSDN

【设计模式】代理模式-原理、实现以及应用场景-CSDN

代理模式-菜鸟教程

代理模式——远程代理(一)-CSDN

CGLIB(Code Generation Library) 介绍与原理

Java中类加载器-CSDN

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
有效的括号 一、题目 给定一个只包括 '(',')','{','}'...
【Ctfer训练计划】——(三... 作者名:Demo不是emo  主页面链接:主页传送门 创作初心ÿ...