写点什么

深度解析 Java 静态代理与动态代理模式的实现

  • 2022 年 7 月 26 日
  • 本文字数:5553 字

    阅读完需:约 18 分钟

深度解析Java静态代理与动态代理模式的实现

前言

你们是否在编程中经常遇见这样一个问题,对于访问某个对象,我们希望给它的方法前加入一个标记,比如对象的方法开始执行、结束等等(比如日志记录)。怎么办呢,这个时候只要我们编写一个复制的类,然后把这个对象传给这个类,再对这个类进行操作,不就可以了吗。这就是代理模式,复制的类就是代理对象,通过代理对象与我们进行打交道就可以对它原来的对象进行改造。对于有些时候现有的对象不能满足我们的需求的时候,如何对它进行扩展,对方法进行改造,使其适用于我们所面临的问题,这就是代理模式的思维出发点。


目录一:代理模式的介绍


二:实现静态代理


三:代理的进阶:实现动态代理


四:总结


接下来按照目录,我们来依次讲解本篇博客:


一:代理模式的介绍


1.1:目标


为其他对象提供一种代理以控制对这个对象的访问


解释:在实际编程中我们会产生一个代理对象,然后去引用被代理对象,对被代理对象进行控制与访问,实现客户端对原代理对象的访问,详情见下面的代码示例。


1.2:适用性


在需要用比较通用和复杂的对象指针代替简单的指针的时候,使用 Proxy 模式。下面是一 些可以使用 Proxy 模式常见情况:1.2.1:远程代理(Remote Proxy )为一个对象在不同的地址空间提供局部代表。 NEXTSTEP[Add94] 使用 N X P r o x y 类实现了这一目的。1.2.2:虚代理(Virtual Proxy )根据需要创建开销很大的对象。在动机一节描述的 I m a g e P r o x y 就是这样一种代理的例子。1.2.3: 保护代理(Protection Proxy )控制对原始对象的访问。保护代理用于对象应该有不同 的访问权限的时候 1.2.4: 智能指引(Smart Reference )取代了简单的指针,它在访问对象时执行一些附加操作。


1.3:结构



实现静态代理


2.1:代码场景


假如我们现在由以下的场景:文件编辑器要对一个图像文件进行操作,遵循以下顺序:加载,绘制,获取长度和宽度,存储四个步骤,但是有个问题,需要被加载的图片非常大,每次加载的时候都要耗费很多时间。并且我们希望对图片的操作可以记录出来操作的步骤,比如第一步、第二步这样便于我们去理解。为了解决这个问题,我们可以先考虑解决第一个问题就是利用代理模式去新建一个代理对象,然后在代理对象里去实现一个缓存,这样下次我们直接可以去缓存里面取对象,而不用去新建,这样就省去了新建对象消耗的资源。另一方面,我们可以考虑去引用原来的方法,再给这方法基础上添加我们所要做的记录。接下里我们用 java 代码来实现这个场景:


2.2:代码示范


2.2.1:首先新建一个接口,命名为 Graphic,其中主要规范了我们进行操作的步骤


public interface Graphic {


void load();//加载
void Draw();//绘制
Extent GetExtent();//获取长度和宽度
void Store();//存储
复制代码


}2.2.2:然后去新建一个 Image 类,用于实现接口,对操作进行具体控制,注意为了其中的 Extent 是对宽度和长度的封装(省略 get 和 set 方法)


public class Image implements Graphic{


public Image() {        try {        Thread.sleep(2000); //模拟创建需要花费很久的时间        System.out.println("正在创建对象");    } catch (Exception e) {                e.printStackTrace();    }}
@Overridepublic void load() { System.out.println("进行加载..");
}
@Overridepublic void Draw() { System.out.println("进行绘画.."); }
@Overridepublic Extent GetExtent() {
Extent extent = new Extent("100","200"); System.out.println("获取图片的属性是:"+extent.toString()); return extent;}


@Overridepublic void Store() {
System.out.println("图片进行存储在硬盘里.."); }
复制代码


}public class Extent {


private String width;
private String length;
public Extent(String width, String length) { super(); this.width = width; this.length = length;}
复制代码


//getter And setter 方法}2.2.3:接下来就是很关键的一步了,新建我们的代理类,我们新建一个类叫做 ImageProxy,然后实现缓存与记录的效果:


import java.util.HashMap;import java.util.Map;


public class ImageProxy implements Graphic{


private Image image;
private Map<String , Image> cache = new HashMap<String, Image>();//缓存
public ImageProxy() { init();}
public void init(){ //只需要初始化一次 if (image==null) { image= new Image(); cache.put("image", image);//放入缓存 }else{
image=cache.get("image");}

@Overridepublic void load() { System.out.println("---第一步开始---"); image.load(); System.out.println("---第一步结束---");}
@Overridepublic void Draw() { System.out.println("---第二步开始---"); image.Draw(); System.out.println("---第二步结束---");}
@Overridepublic Extent GetExtent() { System.out.println("---第三步开始---"); Extent extent = image.GetExtent(); System.out.println("---第三步结束--"); return extent; }@Overridepublic void Store() { System.out.println("---第四步开始---"); image.Store(); System.out.println("---第四步结束--"); }
复制代码


}2.2.4:我们的文档编辑器现在要开始进行文档编辑了,我们来实现具体的代码,我们先来引用一下原对象,看一下原来的对象会出现什么情况:


public class DocumentEditor {


public static void main(String[] args) {         Graphic proxy = new Image();//引用代码          proxy.load();          proxy.Draw();          proxy.GetExtent();          proxy.Store();    }
复制代码


}2.2.5:测试代码


正在创建对象进行加载..进行绘画..获取图片的属性是:Extent [width=100, length=200]图片进行存储在硬盘里..我们可以看出,它会消耗 3 秒才会出来具体的对象,并且没有我们所需要的记录。好了,我们把 2.2.4 的引用代码改为: Graphic proxy = new ImageProxy();


我们再来测试一下:


正在创建对象---第一步开始---进行加载..---第一步结束------第二步开始---进行绘画..---第二步结束------第三步开始---获取图片的属性是:Extent [width=100, length=200]---第三步结束-----第四步开始---图片进行存储在硬盘里..---第四步结束--很明显可以看出,通过访问我们的代理对象,就可以实现对原方法的改造,这就是代理模式的精髓思想。不过到这里你可能会问,为什么不对原对象进行改造呢?为什么要给他新建一个代理对象,这不是很麻烦吗。回答这个问题,首先要提一个代码的设计原则,也就是有名的开闭原则:对扩展开放,对修改关闭。这句话的意思就是不建议对原有的代码进行修改,我们要做的事就是尽量不用动原有的类和对象,在它的基础上去改造,而不是直接去修改它。至于这个原则为什么这样,我想其中一个原因就是因为软件体系中牵一发很动全身的事情很常见,很可能你修改了这一小块,然而与此相关的很多东西就会发生变化。所以轻易不要修改,而是扩展。


三:实现动态代理


3.1:静态代理的不足:


通过看静态代理可以动态扩展我们的对象,但是有个问题,在我们进行方法扩展的时候,比如我们的日志功能:每个前面都得写第一步、第二步。如果我们要再一些其他的东西,比如权限校验、代码说明,一个两个方法还好,万一方法成百个呢,那我们岂不是要累死。这就是动态代理要解决的问题,只需要写一次就可以,究竟是怎么实现的呢,接下里我们来一探究竟吧。


3.2:动态代理的准备:


动态代理需要用到 JDk 的 Proxy 类,通过它的 newProxyInstance()方法可以生成一个代理类,我们来通过 jdk 看一下具体的说明,如何使用它:



返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。此方法相当于:



Proxy.newProxyInstance 抛出 IllegalArgumentException,原因与 Proxy.getProxyClass 相同。


参数:loader - 定义代理类的类加载器 interfaces - 代理类要实现的接口列表 h - 指派方法调用的调用处理程序返回:一个带有代理类的指定调用处理程序的代理实例,它由指定的类加载器定义,并实现指定的接口抛出:IllegalArgumentException - 如果违反传递到 getProxyClass 的参数上的任何限制 NullPointerException - 如果 interfaces 数组参数或其任何元素为 null,或如果调用处理程序 h 为 null


从中可以看出它有三个参数,分别是 classlcoder、interface、InvocationHandler.只要我们把这三个参数传递给他,它就可以 返回给我们一个代理对象,访问这个代理对象就可以实现对原对象的扩展。接下来,我们用代码来实现它。


3.3:代码场景


我们来做这样一个场景,我们实现一个计算器,计算器里面有加减乘除方法,然后我们实现这个计算的接口,有具体的类和被代理的类,我们通过动态代理来生成代理类,而不用自己去建了,好了,看接下来的代码:


3.4:动态代理的代码实现


3.4.1:首先我们新建一个接口,命名为 Calculator ,声明四个方法:


public interface Calculator {


int add(int i,int j);//加
int sub(int i,int j);//减
int mul(int i,int j);//乘
double div(int i,int j);//除
复制代码


}3.4.2:新建一个实现类,命名为 CalculatorImpl ,也就是被代理类


public class CalculatorImpl implements Calculator{


@Overridepublic int add(int i, int j) {        return i+j;}
@Overridepublic int sub(int i, int j) { return i-j;}
@Overridepublic int mul(int i, int j) { return i*j;}
@Overridepublic double div(int i, int j) { return (i/j);
}
复制代码


}3.4.3:新建一个类,命名为 CalCulatorDynamicProxy,也就是我们的代理类,用来对上面的类进行代理:


import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.util.Arrays;


public class CalCulatorDynamicProxy { //动态代理类


private Calculator calculator;//要代理的对象
public CalCulatorDynamicProxy(Calculator calculator) {
this.calculator = calculator;}
public Calculator getCalculator() {
Calculator proxy = null;
ClassLoader loader =calculator.getClass().getClassLoader();//获取类加载器
Class[] interfaces = new Class[]{Calculator.class};//代理对象的类型
InvocationHandler h = new InvocationHandler() {//调用处理器
//proxy:正在返回的代理对象 //method:被调用的方法 //args:传入的参数 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("---日志记录开始---"); String name = method.getName();//获取方法的名字 System.out.println("方法"+name+"()开始执行了"); System.out.println("方法中的参数是:"+Arrays.asList(args)); Object result = method.invoke(calculator, args); System.out.println("方法执行后的结果是"+result); return result; } }; proxy=(Calculator)Proxy.newProxyInstance(loader, interfaces, h);//代理对象 return proxy;}
复制代码


}这里要特别强调的问题就是:invoke()方法,注意其中的参数,分别是被代理对象、方法、和对象参数,这里的原理是反射,通过获取原对象的 class 对象,然后进行处理,我们可以通过 method 对象拿到被代理对象的方法,也是 add()、mul()、sub()、div()方法,也可以通过 args 对象数组取得传入的参数,比如我们具体传入的数值,再通过 method.invoke()方法进行调用,就进行了被代理对象的方法的执行,然后就是返回的结果(如果方法前为 void,返回的就是 null)


3.4.4:我们来做具体的测试


public class Test {


public static void main(String[] args) {
Calculator cal = new CalculatorImpl(); Calculator proxy = new CalCulatorDynamicProxy(cal).getCalculator(); int add = proxy.add(29, 1); int sub = proxy.sub(9, 2); int mul = proxy.mul(3, 7); double div = proxy.div(6,8); }
复制代码


}具体的测试结果:


---日志记录开始---方法 add()开始执行了方法中的参数是:[29, 1]方法执行后的结果是 30---日志记录开始---方法 sub()开始执行了方法中的参数是:[9, 2]方法执行后的结果是 7---日志记录开始---方法 mul()开始执行了方法中的参数是:[3, 7]方法执行后的结果是 21---日志记录开始---方法 div()开始执行了方法中的参数是:[6, 8]方法执行后的结果是 0.0 可以看出动态代理模式轻松完成了对被代理对象的日志记录功能,并且只用写一次,这样即便有成百上千的方法我们也不怕,这就是动态代理领先于静态代理之处,虽然实现起来有点麻烦,但是其方便,动态的给被代理对象添加功能。我们所写的重复代码更少,做的事情更少。


四:总结


本篇博客介绍了动态代理和静态代理的概念,并对其进行了代码实现,在实际的工作中,我们会经常遇到需要代理模式的地方,希望能多多思考,促进我们形成一定的思维模式。并且动态代理作为 SpringAop 的实现原理,封装了动态代理,让我们实现起来更加方便,对于这部分内容可以只做了解,理解其背后的运行机制即可,并不需要具体实现,如果需要实现,直接使用 spring 的 Aop 功能即可。


希望看完本篇,能对代理这种思维有深入的理解。好了,本篇文章就讲到这里,谢谢。

用户头像

不定期更新Java开发工具及Java面试干货技巧 2021.12.12 加入

Java后端工程师,十年大厂经验。具有扎实的Java、JEE基础知识。熟悉Spring、SpringMVC、Struts MyBatisHibernate等JEE常用框架。

评论

发布
暂无评论
深度解析Java静态代理与动态代理模式的实现_Java_了不起的程序猿_InfoQ写作社区