您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

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

时间:2022-07-26 14:12:55  来源:  作者:秃头Java人

前言

大家好,我是秃头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:结构

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

 

二:实现静态代理

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();
        }
    }
    
    @Override
    public void load() {
        
        System.out.println("进行加载..");

    }

    @Override
    public void Draw() {
        
        System.out.println("进行绘画..");
        
    }

    @Override
    public Extent GetExtent() {

        Extent extent = new Extent("100","200");
        
        System.out.println("获取图片的属性是:"+extent.toString());
        
        return extent;
    }



    @Override
    public 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");
    }
    

    @Override
    public void load() {
        
         System.out.println("---第一步开始---");
         
          image.load();
         
         System.out.println("---第一步结束---");
    }
    
    @Override
    public void Draw() {
        
        System.out.println("---第二步开始---");
        
        image.Draw();
        
        System.out.println("---第二步结束---");
    }

    @Override
    public Extent GetExtent() {
        
        System.out.println("---第三步开始---");
        Extent extent = image.GetExtent();
        System.out.println("---第三步结束--");
        
        return extent;
        
    }
    @Override
    public 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看一下具体的说明,如何使用它:

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
                               throws IllegalArgumentException

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

   Proxy.getProxyClass(loader, interfaces).
         getConstructor(new Class[] { InvocationHandler.class }).
         newInstance(new Object[] { handler });
 

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{

    @Override
    public int add(int i, int j) {
        
        return i+j;
    }

    @Override
    public int sub(int i, int j) {
        
        return i-j;
    }

    @Override
    public int mul(int i, int j) {
        
        return i*j;
    }

    @Override
    public 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功能即可。

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



Tags:静态代理   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
搞懂Java三种代理模式:静态代理、动态代理和cglib代理
Java静态代理Java中的静态代理是一种设计模式,它通过创建一个代理类来代替原始类,从而控制对原始类的访问。代理类和原始类都实现相同的接口,使得客户端在使用时无需关心具体的...【详细内容】
2023-08-09  Search: 静态代理  点击:(288)  评论:(0)  加入收藏
深度解析Java静态代理与动态代理模式的实现
前言大家好,我是秃头Java人。你们是否在编程中经常遇见这样一个问题,对于访问某个对象,我们希望给它的方法前加入一个标记,比如对象的方法开始执行、结束等等(比如日志记录)。怎么...【详细内容】
2022-07-26  Search: 静态代理  点击:(373)  评论:(0)  加入收藏
java静态代理与动态代理
1. 静态代理与动态代理区别静态代理:需要编写批量代理类动态代理:不需要编写批量代理类2.静态代理静态代理结构如下:编写测试接口:public interface TestInteface { /** * 测试...【详细内容】
2019-12-09  Search: 静态代理  点击:(483)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(18)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(25)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(34)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(63)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(78)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(78)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(100)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(111)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(108)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(82)  评论:(0)  加入收藏
站内最新
站内热门
站内头条