设计模式-代理模式

2023-09-14 22:57:40

“阁下有什么问题可以和我的代理律师谈即可”,为什么会有律师这个职业呢?随着法律法规的逐步完善,日益复杂,导致大部分的普通民众掌握的法律知识明显不足,进而无法在合适的时间点进行维权、规避风险。可见,律师的作用就是利用自身的专业知识帮助案件当事人处理其无法处理的事情。不仅只有律师,生活中处处可见这种代理模式的存在,比如婚庆公司、各种中介(房产、婚姻)等等。这些职业、公司等都是起到了帮助当事人处理一些比较专业的事情,而当事人只需要在合适的时间点做出其应该做的动作即可。因此,读者在需要委托代理他人做事情时,一定要问清楚,需要代理做什么?自身要做什么?哈哈,话题扯远了。那么在代码世界中有没有类似的代理需求呢?答案是肯定是有的。比如说打印日志、事务、流量统计、限流、鉴权等。这些事情均不应该由目标对象(如接口、服务)负责,但是在一些场景下,又和目标对象行为密切相关,那这时我们就需要引入代理对象来负责完成目标对象所不能完成的事情。实现这种代理行为的代码组织解决方案就是代理模式。在JAVA语言中,代理模式根据编译期是否已声明代理类信息可分为两种:静态代理或动态代理。

一、静态代理

为保证概念清晰,这里提前将下文一些指代名词解释如下:

  • 被代理的类称之为被代理类;其实例化的对象称之为被代理对象。
  • 用于实现代理被代理类的类称之为代理类;其实例化的对象称之为代理对象。

静态代理是指在编译期就已经定义了代理类信息。代理类信息应该包括哪些呢?
① 既然能够代理,就意味着代理类拥有被代理类所有的可执行方法(或行为)
② 为达到代理的目的,代理类执行被代理可执行方法时,会在合适时机(之前或之后)执行其特有的代理行为
我们能够很自然地想到,在面向对象语言中,代理类使用组合或继承(被代理类)的方式能够十分容易满足这两点。

1.1 静态代理-组合

我们先来看通过“组合”的方式如何实现静态代理。组合模式一般是指整体和部分之间的关系,用于表示1(整体)-N(部分)的关系。具体应用到代理模式上,1即为代理类(对象),而N=1即为被代理类(对象)。那么,组合的方式如何满足前面提到的代理类信息的两点要求:
① 代理类拥有被代理类所有的可执行方法
使用组合的方式,我们必须手动给代理类添加所有被代理类的可执行方法。但是,这种没有设计层面的约束,很容易出现问题。如被代理类新增方法时,代理类很容易忽略而没有新增该方法,破坏了代理模式。
因此,使用组合的方式一般强制要求代理类实现接口,接口用于表示需要被代理的方法。被代理类也需要实现该接口即可满足要求。【ps:JDK动态代理要求被代理类必须实现接口】
② 代理类执行被代理可执行方法时,会在合适时机(之前或之后)执行其特有的代理行为
为了实现代理功能,代理类在执行任何代理方法时,必须显示调用(执行)被代理类(对象)的对应被代理方法
根据以上分析,我们下面给出简单的使用组合方式实现静态代理模式的代码&类图:
代理接口:

public interface ProxyInterface {
    void proxyMethod1();
    void proxyMethod2();
    void proxyMethodX();
}

被代理类:

public class CustomClass implements ProxyInterface{

    @Override
    public void proxyMethod1() {
        System.out.println("执行被代理方法,methodName=proxyMethod1");
    }

    @Override
    public void proxyMethod2() {
        System.out.println("执行被代理方法,methodName=proxyMethod2");
    }

    @Override
    public void proxyMethodX() {
        System.out.println("执行被代理方法,methodName=proxyMethodX");
    }
}

代理类:

public class Proxy implements ProxyInterface{

    private final ProxyInterface obj;

    public Proxy(ProxyInterface obj) {
        // 通过构造函数约束,创建代理对象传入被代理对象
        this.obj = obj;
    }

    @Override
    public void proxyMethod1() {
        System.out.println("代理前置逻辑...");
        obj.proxyMethod1();     // 调用被代理对象的代理方法
        System.out.println("代理后置逻辑...");
    }

    @Override
    public void proxyMethod2() {
        System.out.println("代理前置逻辑...");
        obj.proxyMethod1();
        System.out.println("代理后置逻辑...");
    }

    @Override
    public void proxyMethodX() {
        System.out.println("代理前置逻辑...");
        obj.proxyMethod1();
        System.out.println("代理后置逻辑...");
    }
}

在这里插入图片描述
使用组合方式实现静态代理模式的优点:

  • 可以通过接口来指明需要代理的方法。
  • 从对象代理角度上来看,组合方式更加符合代理定义,因为被代理的是存在的对象。

缺点:

  • 如果需要被代理类很多,需要定义很多代理接口及代理类,引起类数量的剧增。

1.1 静态代理-继承

在面向对象语言中可以通过继承目标类,进而重写目标方法来实现代理(实际上,这种代理更准确的说是增强)。继承的方式要求代理类重写代理方法并通过调用父类方法来执行实际逻辑,而在调用父类方法之前或之后会执行代理行为。
① 代理类拥有被代理类所有可执行方法
继承机制保证了这一点,代理类一定会存在被代理类所有可执行方法。默认情况下代理类行为与被代理类行为含义一致。
② 代理类执行被代理可执行方法时,会在合适时机(之前或之后)执行其特有的代理行为
为了实现代理功能,代理类(子类)需在执行代理方法时执行被代理类(父类)的被代理方法。
根据以上分析,我们下面给出简单的使用继承方式实现静态代理模式的代码&类图:
被代理类:

public class CustomClass{

    public void proxyMethod1() {
        System.out.println("执行被代理方法,methodName=proxyMethod1");
    }

    public void proxyMethod2() {
        System.out.println("执行被代理方法,methodName=proxyMethod2");
    }

    public void proxyMethodX() {
        System.out.println("执行被代理方法,methodName=proxyMethodX");
    }
}

代理类:

public class Proxy extends CustomClass{
    @Override
    public void proxyMethod1() {
        System.out.println("代理前置逻辑...");
        super.proxyMethod1();
        System.out.println("代理后置逻辑...");
    }

    @Override
    public void proxyMethod2() {
        System.out.println("代理前置逻辑...");
        super.proxyMethod2();
        System.out.println("代理后置逻辑...");
    }

    @Override
    public void proxyMethodX() {
        System.out.println("代理前置逻辑...");
        super.proxyMethodX();
        System.out.println("代理后置逻辑...");
    }
}

在这里插入图片描述
使用继承方式实现静态代理模式的优点:

  • 不要求被代理类必须实现所有接口,通过继承机制,存在默认代理逻辑。
  • 继承方式更像是类增强,不需存在被代理对象。

缺点:

  • 如果需要被代理类很多,需要定义很多代理类,引起类数量的剧增。

二、动态代理

从前面可知,静态代理的最大的缺点就是当需要被代理类很多时,就需要定义许多代理类,逐渐引起类数量的剧增。那有没有可能存在一种方式能够自动生成代理类并实现代理功能呢?答案就是动态代理。
在JAVA中,动态代理是一种机制,通过动态代理机制实现在运行时根据需要动态生成代理类,从而达到在不修改源代码的情况下,为指定类提供额外的代理功能。这种动态代理机制主要有两种:JDK动态代理、CGLIB生成代理。

2.1 JDK动态代理

我们先来回顾下JDK动态代理的基本使用:

// 实例化自定义调用处理器实例
InvocationHandler handler = new MyInvocationHandler(...);
// 获取代理对象方式一 
Class<?> proxyClass = Proxy.getProxyClass(ProxyInterface.class.getClassLoader(), ProxyInterface.class);
ProxyInterface proxyV1 = (ProxyInterface) proxyClass.getConstructor(InvocationHandler.class).
                newInstance(handler);
// 获取代理对象方式二
ProxyInterface proxyV2 = (ProxyInterface) Proxy.newProxyInstance(ProxyInterface.class.getClassLoader(),
                           new Class<?>[] { ProxyInterface.class },
                           handler);

如上所示,JDK动态代理主要通过java.lang.reflect包中的Proxy类和InvocationHandler接口实现。具体的来说,代理类信息(即Class对象)可通过Proxy类的静态方法getProxyClass创建并获取。代理对象既可以通过代理类的构造方法创建也可以通过Proxy类的静态方法newProxyInstance直接获取。
代理类信息的创建需要传入代理接口类加载器及代理接口,其中,不同类加载器生成的代理类均唯一,代理接口指定了代理类需要代理的方法。代理对象的创建需要传入InvocationHandler对象,InvocationHandler接口仅有一个invoke方法,该方法内部就是代理执行的动作。
为了弄清楚JDK动态代理具体实现逻辑,通过设置“sun.misc.ProxyGenerator.saveGeneratedFiles”属性为True生成代理类文件。

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

生成的代理类信息如下:

public final class $Proxy0 extends Proxy implements Person {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void ageNum() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("com.design.代理模式.jdkDynamic.Person").getMethod("ageNum");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

通过代理类信息,我们可以看到代理类会实现Object类的equals、toString、hashCode以及代理接口所有方法。代理对象执行代理方法内部实际上会调用InvocationHandler对象的invoke方法来执行实际逻辑。因此,具体代理对象的方法执行逻辑都有invoke方法来控制,invoke方法有三个参数:① 当前代理对象 ② 当前执行方法Method对象 ③ 方法参数列表。
为了实现代理,InvocationHandler对象的invoke方法中我们会调用被代理对象的Method方法。注意,这里不能调用代理对象的Method方法,否则会出现循环调用。因此,我们在创建InvocationHandler对象时,会通过构造方法或Setter方法传入代理对象。
实际上,从代理模式含义上来说,代理对象直接代理的是InvocationHandler对象,而InvocationHandler对象又直接代理被代理对象,并且两部代理均为组合方法的代理。
在这里插入图片描述
如上图所示,JDK动态代理实际上是通过两层组合方式的代理最终实现代理对象(DynamicProxy)与被代理对象(CustomClass)之间的代理关系。
JDK动态代理的优点:

  • 通过动态生成代理类,减少了代理代码的重复性,提高代码可读性
  • 使得代理部分代码和业务代码解耦,增加代码可维护性
  • JDK动态代理具有很好的可扩展性

JDK动态代理的缺点:

  • 所有方法的被代理均有一个InvocationHandler对象实现,无法针对不同方法实现不同代理逻辑。
  • JDK动态代理,如同组合方式的代理一样,被代理类要求必须继承接口。【实际创建代理类时也必须传入接口Class】

有关于JDK动态代理实现的底层原理,如动态代理类的类信息定义过程、类缓存机制可参考JDK动态代理原理

2.2 CGLIB生成代理

CGLIB(Code Generation Library)实际上是第三方提供强大、高性能的代码生成库。在该库中提供了可用于类增强的工具类-Enhancer类。我们通常就会使用Enhancer类来实现动态代理的功能,相比于JDK动态代理,基于Enhancer类实现的动态代理的对象不局限于接口类。下面我们先回顾下CGLIB动态代理的基本使用:

// 自定义方法拦截器
MethodInterceptor myMethodInterceptor = new MyMethodInterceptor();
// 使用Enhancer创建代理类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Student.class);
enhancer.setCallback(myMethodInterceptor);
Student stu = (Student) enhancer.create();
stu.outStudentName();

如上所示,CGLIB动态生成代理主要是通过第三方包(net.sf.cglib)中的Enhancer类和MethodInterceptor 接口实现。使用Enhancer生成代理对象之前,需要将被代理类Class、执行方法回调的方法拦截器传入Enhancer对象中。我们可以看到,被代理类Class信息实际上是作为父类信息,反映到内部也就是代理类的父类。方法拦截器实际上就是回调方法,即代理类执行方法时会通过MethodInterceptor回调对象的intercept方法进行调用方法逻辑。
Ehancer对象提供了很多类似的setXXX方法,用于对动态生成的代理类实现不同功能。这里值得一提的是,方法拦截器(MethodInterceptor)对象可以设置多个,用于针对不同的方法执行不同的拦截逻辑。对于多个方法拦截器,你还需要指定一个CallbackFilter对象,该对象用于自定义哪些方法使用哪个方法拦截器。
与JDK动态代理类似,你也可以通过设置系统属性来获取动态生成的代理类信息。

// 代理类class文件存入本地磁盘方便我们反编译查看源码
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "E:\\project\\java\\mavenTest");

CGLIB动态代理类比较长,这里就不做赘述了,关于CGLIB代理类信息详细可参考CGLIB原理简析-前篇。下图给出了代理类执行代理方法的逻辑:
在这里插入图片描述
逻辑也十分简单,和之前说的一样,就是通过调用回调对象-也是方法拦截器对象的intecept方法执行实际方法逻辑。因此,具体代理对象的方法执行逻辑都由intecept方法来控制,intecept方法有四个参数:① 代理对象 ② 当前拦截的方法 ③ 方法参数值列表 ④ 代理类的方法代理MethodProxy
为了实现代理,intecept方法中我们一般都会调用MethodProxy的invokeSuper方法,并将代理对象以及方法参数传入来执行代理对象实际业务逻辑。注意这里不能调用MethodProxy的invoke方法,这个方法时调用代理对象的代理方法,会出现循环调用。从invokeSuper的方法名也可以看出来,这实际上是调用父类的方法。因此,从代理模式含义上来说,动态生成的代理类实际上是通过继承的方式来实现的代理。
在这里插入图片描述
如上图所示,CGLIB动态代理实际上是通过继承的方式实现了代理模式。
CGLIB动态代理的优点:

  • 通过动态生成代理类,减少了代理代码的重复性,提高代码可读性
  • 使得代理部分代码和业务代码解耦,增加代码可维护性
  • JDK动态代理具有很好的可扩展性
  • 可以对几乎任何类实现代理,不局限于接口

CGLIB动态代理的缺点:

  • 需要引入第三方依赖包
  • 由于是使用继承方式来实现的代理类,因此无法代理final的类和方法
  • 使用字节码操作的方式创建代理类,可能第一次创建的过程可能会很慢,性能较差。

有关于CGLIB动态代理实现的底层原理,如动态代理类的类信息定义过程、类缓存机制可参考CGLIB原理简析-前篇CGLIB原理简析-后篇

三、总结

通过本文可以了解到,面向对象语言中代理模式的实现从实现方式可分为两种方式:组合和继承。从是否编译器确定代理类角度上看分为静态代理和动态代理。而其中动态代理的实现机制不同可分为JDK动态代理和CGLIB动态代理。在实际使用场景中,我们必须清楚的知道在什么场景下应该选用什么样的代理模式。

3.1 静态代理vs动态代理

什么时候考虑使用静态代理?
静态代理适用于特殊场景,比如仅针对某一个特殊业务功能类来实现代理,建议使用静态代理。比如业务系统存在某本地缓存类,后续技术优化打算加上监控,为了增加每分钟上报本地缓存监控的能力,就可以使用静态代理,而不适合使用动态代理。
实际上,在面向对象语言中,使用组合和继承时,本身就有代理的含义在其中,这也是为什么cglib中Enhancer类能作为一种代理模式机制的原因。从这个角度看,静态代理的实际应用场景实际上要比动态代理更广,更直接。
什么时候考虑使用动态代理:
动态代理适用于普遍场景,即当系统中存在大面积的代理功能需要,如果使用静态代理就会导致类激增问题,并且如一些第三方工具包无法预先知道被代理类Class信息,这时就需要使用动态代理。因此,动态代理在第三方包中更加常见,第三方包需要使用动态代理在其未知业务类信息上进行代理或增强,添加其支持的额外功能。

3.2 组合vs继承

既然要使用代理,那选择组合的方式实现代理,还是选择继承的方式实现代理呢?这个问题同样也能回答JDK动态代理和CGLIB动态代理的选择。

  1. 首先使用继承的方式,由于JAVA语言继承机制的限制,本身就无法对使用final修饰的类进行代理。
  2. 使用继承的方式就无需创建被代理类的对象
  3. 在具体机制上,JDK动态代理仅能对接口进行代理
  4. 在具体机制上,CGLIB动态代理性能可能更差些。
    还有个说法是,JDK动态代理使用反射机制,CGLIB代理使用字节码处理ASM。因此JDK动态代理的创建代理类效率高,但是执行代理方法效率低。CGLIB代理创建效率较低,执行效率因为FastClass机制效率高。但是近些年JDK动态代理的执行效率也不弱,因此很多时候应首选JDK动态代理。
更多推荐

【AIGC】提示词 Prompt 分享

提示词工程是什么?Promptengineering(提示词工程)是指在使用语言模型进行生成性任务时,设计和调整输入提示(prompts)以改善模型生成结果的过程。它是一种优化技术,旨在引导模型产生更加准确、相关和符合预期的输出。在生成性任务中,输入提示是指提供给语言模型的初始文本或问题,用以引导其生成后续的文本或回答

无CDN场景下的传统架构接入阿里云WAF防火墙的配置实践

文章目录1.配置网站接入WAF防火墙1.1.配置网站接入方式1.2.填写网站的信息1.3.WAF防火墙生成CNAME地址2.配置WAF防火墙HTTPS证书3.修改域名DNS解析记录到WAF防火墙4.验证网站是否接入WAF防火墙传统架构接入WAF防火墙非常简单,配置完WAF网站接入后,将得到CNAME地址配置在域名DNS

Linux(Centos7)中安装Docker和DockerCompose

一、安装DockerDocker分为CE和EE两大版本。CE即社区版(免费,支持周期7个月),EE即企业版,强调安全,付费使用,支持周期24个月。DockerCE分为`stable``test`和`nightly`三个更新频道。官方网站上有各种环境下的https://docs.docker.com/install/,这

【Rust 基础篇】Rust Newtype模式:类型安全的包装器

导言Rust是一种以安全性和高效性著称的系统级编程语言,其设计哲学是在不损失性能的前提下,保障代码的内存安全和线程安全。在Rust中,Newtype模式是一种常见的编程模式,用于创建类型安全的包装器。Newtype模式通过定义新的结构体包装器来包装现有的类型,从而在不引入运行时开销的情况下提供额外的类型安全性。本篇博客

xshell---git上传文件到gitee远程仓库配置

1.git下载如果没有xshell下没有下载过git,可以参考这篇的教程:Linux配置安装git详细教程下载后可以通过git--version查看git的版本号,验证是否安装成功2.新建仓库首先需要在gitee上注册一个账号然后再主页面点击右上边框的+号,选择新建仓库,建立一个仓库:然后填写新建仓库的名称,系统会根据

GIT使用需知,哪些操作会导致本地代码变动

系列文章目录手把手教你安装Git,萌新迈向专业的必备一步GIT命令只会抄却不理解?看完原理才能事半功倍!常用GIT命令详解,手把手让你登堂入室GIT实战篇,教你如何使用GIT可视化工具GIT使用需知,哪些操作会导致本地代码变动系列文章目录一、本地代码变动的本质1.远程跟踪分支2.贮藏区(stash)二、分支切换三、分支

(vue的入门

vue的入门一.Vue是什么二.Vue的特点及优势三.使用Vue的详细步骤四.Vue的基本语法五.Vue的生命周期一.Vue是什么Vue(发音为/“vjuː”/,类似于"view")是一套用于构建用户界面的渐进式JavaScript框架。它是一个开源的、轻量级的MVVM(模型-视图-视图模型)框架,专注于实现数据驱动的

vue +element 删除按钮操作 (删除单个数据 +删除页码处理 )

1.配置接口deleteItemById:"/api/goods/deleteItemById",//删除商品操作2.get请求接口//删除接口后台给我们返iddeleteItemById(params){returnaxios.get(base.deleteItemById,{params})}3.异步请求接口asy

如何在Gazebo中实现多机器人编队仿真

文章目录前言一、仿真前的配置二、实现步骤1.检查PC和台式机是否通讯成功2.编队中对单个机器人进行独立的控制3、对机器人进行编队控制前言实现在gazebo仿真环境中添加多个机器人后,接下来进行编队控制,对具体的实现过程进行记录。一、仿真前的配置本文的多机器人编队,在turtlebot3单个机器人的建图、导航等功能的基础

Linux设备驱动模型之SPI

Linux设备驱动模型之SPISPI:SerialPeripheralInterface,串行外设接口,主要用于控制器与外部传感器进行数据通信的接口,它是一种同步、全双工、主从式接口。SPI接口介绍接口定义SPI接口有4根信号线,分别是片选信号、时钟信号、串行输出数据线、串行输入数据线。SS:从设备使能信号,由SPI主

固定资产管理系统都有哪些功能呢

固定资产管理系统作为企业资产管理的重要工具,具有提高效率、降低成本、保证资产合理使用的多种功能。以下是一些典型的功能:资产登记和信息管理:系统可以自动记录公司的固定信息,包括资产名称、型号、购买日期、原始价值、折旧方法、折旧年限等。同时,系统还支持自动更新和查看资产信息。资产申请和偿还:员工可以通过平台申请或偿还资产,

热文推荐