美文网首页
设计模式——代理模式

设计模式——代理模式

作者: Qiansion齐木楠雄 | 来源:发表于2021-05-27 17:48 被阅读0次

    概述

    定义:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。

    Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。

    结构

    代理(Proxy)模式分为三种角色:
    抽象角色(Subject)类: 通过接口或抽象类声明真实角色和代理对象实现的业务方法。
    真实角色(Real Subject)类: 实现了抽象角色中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
    代理(Proxy)类 : 提供了与真实角色相同的接口,其内部含有对真实角色的引用,它可以访问、控制或扩展真实主题的功能。

    还是拿最近的事来说说吧。最近本人想买一套洗面奶系列的产品,想试试国外的产品,因为我没有国外商家的联系渠道,就找了一些能卖洗面奶的产品(抽象角色类)的代理,突然想到我的姐姐是做海外代购的,所以我就直接找她。真实角色类就是海外的商家,因为洗面奶是由商家提供的,由于我姐姐能联系海外的商家方式(在代码里体现为一种聚合关系),所以形成了代理和被代理的关系,所以我的姐姐可以认为是一个代理类
    以上例子的类图如下:

    image.png

    使用代理的好处

    中介隔离作用:在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
    开闭原则,增加功能:代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。

    静态代理

    我们先用静态代理复现一下上面的场景
    ①创建抽象角色类(此处为接口)

    interface SellPorduct{
        void sellBiotherm();
    }
    

    ②创建真实角色类(海外商家)

    class OverseasStore implements SellPorduct{
        @Override
        public void sellBiotherm() {
            System.out.println("卖出洗面奶——此消息来自供应商");
        }
    }
    

    ③创建代理类(我的姐姐)

    class SisterProxy implements SellPorduct {
    
        private SellPorduct seller;
    
        public void setOverseasStore(SellPorduct seller) {
            this.seller = seller;
        }
    
        @Override
        public void sellBiotherm() {
            seller.sellBiotherm();
        }
    }
    

    ④我找我姐姐买洗面奶

    public class AppStaticTest {
        public static void main(String[] args) {
            OverseasStore overseasStore = new OverseasStore();
            SisterProxy sisterProxy = new SisterProxy();
            sisterProxy.setOverseasStore(overseasStore);
            sisterProxy.sellBiotherm();
        }
    }
    

    卖出洗面奶——此消息来自供应商
    可能有人会对②中有疑问:为什么私有变量是一个接口呢?这是因为依赖倒置原则中说过,上层不能直接依赖具体实现,要依赖抽象。
    可能有人会对④中有疑问:既然都创建了OverseasStore的实例,为什么不直接调用overseasStore.sellBiotherm()呢?这是因为前提是我知道有海外商家,但我并不能联系海外商家。如果我能联系海外商家,那我也成代理了 = =。

    现在姐姐可以帮我买到洗面奶,但是商家不知道我的住址,无法把快递发给我,此时姐姐也可以帮我完成这些功能。有了代理类,我们需要增加一些功能也可以在代理类中添加,就不必去修改原来的代码。

    class SisterProxy implements SellPorduct {
    
        private SellPorduct seller;
    
        public void setOverseasStore(SellPorduct seller) {
            this.seller = seller;
        }
    
        @Override
        public void sellBiotherm() {
            address();
            seller.sellBiotherm();
            send();
        }
    
        private void address(){
            System.out.println("得到收货地址");
        }
    
        private void send(){
            System.out.println("快递已发出");
        }
    }
    

    再次执行main方法
    得到收货地址
    卖出洗面奶——此消息来自供应商
    快递已发出
    优点
    在不修改目标对象的前提下,能通过代理对象对目标功能拓展
    缺点
    ①因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类产生。
    ②一旦接口增加方法,目标对象与代理对象都需要维护。
    举例:对于①,现在我是在买洗面奶需要姐姐代理,如果以后我想租房,就得找其他人去代理了,因为姐姐并不是房产代理。对于②,这次是买洗面奶,如果下次我想要买面膜就需要抽象角色类中添加买面膜的方法,同时需要在海外商家和sisterProxy中添加相应的方法。

    JDK动态代理

    动态代理,顾名思义就是“动态”的,这种动态提现在哪儿呢?我们带着问题从代码中入手。在上面的代码中,我们将静态代理的SisterProxy换成如下的动态代理(此处使用静态内部类的形式):

    class DynamicProxy{
        private Object target;
    
        public DynamicProxy(){}
    
        public void setTarget(Object target) {
            this.target = target;
        }
    
        public Object getTarget() {
            return target;
        }
    
        public DynamicProxy(Object target){
            this.target = target;
        }
    
        public Object getProxyInstance(){
            return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    address();
                    Object object=method.invoke(target,args);
                    send();
                    return object;
                }
            });
        }
    
        private void address(){
            System.out.println("得到收货地址");
        }
    
        private void send(){
            System.out.println("快递已发出");
        }
    }
    

    修改main方法并执行

    public class AppDynamicTest {
        public static void main(String[] args) {
            DynamicProxy dynamicProxy = new DynamicProxy();
            OverseasStore overseasStore = new OverseasStore();
            dynamicProxy.setTarget(overseasStore);
            SellPorduct instance = (SellPorduct) dynamicProxy.getProxyInstance();
            instance.sellBiotherm();
        }
    }
    

    得到收货地址
    卖出洗面奶——此消息来自供应商
    快递已发出
    依然得到与之前相同的结果。
    这里我们主要看getProxyInstance这个方法

        public Object getProxyInstance(){
            return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    address();
                    Object object=method.invoke(target,args);
                    send();
                    return object;
                }
            });
        }
    

    这里有两个地方需要重点关注:

    Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
    //
    new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    address();
                    Object object=method.invoke(target,args);
                    send();
                    return object;
             }
    }
    

    Proxy.newProxyInstance是创建代理的方法。这里要说一下它的三个参数。
    在讲解之前有必要的提一嘴关于JVM运行原理

    JVM运行原理

    image.png

    运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。

    "动态"代理的动态性

    一般地,java源码文件在编译阶段生成字节码文件然后在运行时用类加载器进行加载进内存。动态代理的“动态“在于,它是在运行期间直接向内存中写入字节码文件。
    流程图如下:


    image.png

    现在我们在看一下它的三个参数

    ClassLoader loader:
    我们都知道,要实例化一个对象,是需要调用类的构造器的,在程序运行期间第一次调用构造器时,就会引起类的加载,加载类的时候,就是jvm拿着classLoader去加载类的字节码的,只有字节码被加载到了内存中,才能进一步去实例化出类的对象。 简单来说,就是只要涉及到实例化类的对象,就一定要加载类的字节码,而加载字节码就必须使用类加载器。下面我们使用的是动态代理的api来创建一个类的对象。这是一种不常用的实例化类对象的方式,尽管不常用,但毕竟涉及实例化类的对象那就一定也需要加载类的字节码,也就一定需要类加载器,所以我们手动把类加载器传入
    Class<?>[] interfaces
    动态代理在内存中“动态“生成的! 要知道,这个在内存中直接生成的字节码,会去自动实现此处所指定的接口!所以,利用动态代理生产的对象,就能转成SellPorduct接口类型!那么这个代理对象就拥有sellBiotherm()方法
    InvocationHandler h
    调用处理器,对于代理对象执行的所有方法都会在InvocationHandler的invoke方法中进行处理。

    对于上面的InvocationHandler举例说明一下:代理对象执行任何方法都会在invocationHandler的invoke方法中进行处理
    我们可以修改代码如下

    class DynamicProxy{
        private Object target;
    
        public DynamicProxy(){}
    
        public void setTarget(Object target) {
            this.target = target;
        }
    
        public Object getTarget() {
            return target;
        }
    
        public DynamicProxy(Object target){
            this.target = target;
        }
    
        public Object getProxyInstance(){
            return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("proxy:代理方法统一处理入口");
    //                address();
    //                Object object=method.invoke(target,args);
    //                send();
                    return null;
                }
            });
        }
    
        private void address(){
            System.out.println("得到收货地址");
        }
    
        private void send(){
            System.out.println("快递已发出");
        }
    }
    

    执行main方法

    public class AppStaticTest {
        public static void main(String[] args) {
            OverseasStore overseasStore = new OverseasStore();
            DynamicProxy dynamicProxy = new DynamicProxy();
            dynamicProxy.setTarget(overseasStore);
            SellPorduct proxyInstance = (SellPorduct) dynamicProxy.getProxyInstance();
            proxyInstance.sellBiotherm();
        }
    }
    

    proxy:代理方法统一处理入口
    结果与预期的一致。
    为什么代理类执行方法时会跳到内部类的invoke方法呢?
    我们可以看一下产生的代理类长什么样子。在main方法中添加

    System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles","true");      
    // 上面的没启作用的,用下面这个
    System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
    

    运行main方法会在根目录下生成com的文件包,点开之后就能看到$Proxy0的字节码

    final class $Proxy0 extends Proxy implements SellPorduct {
        private static Method m1;
        private static Method m3;
        private static Method m2;
        private static Method m4;
        private static Method m0;
    
        public $Proxy0(InvocationHandler var1) throws  {
            super(var1);
        }
    
        public final boolean equals(Object var1) throws  { }
    
        public final void sellBiotherm() throws  {
            try {
                super.h.invoke(this, m3, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
    
        public final String toString() throws  { }
    
        public final int hashCode() throws  { }
    
        static {
            try {
                m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
                m3 = Class.forName("com.company.proxy.SellPorduct").getMethod("sellBiotherm");
                m2 = Class.forName("java.lang.Object").getMethod("toString");
                m4 = Class.forName("com.company.proxy.SellPorduct").getMethod("sellOthers");
                m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            } catch (NoSuchMethodException var2) {
                throw new NoSuchMethodError(var2.getMessage());
            } catch (ClassNotFoundException var3) {
                throw new NoClassDefFoundError(var3.getMessage());
            }
        }
    }
    

    我们可以看出在$Proxy0的构造方法中,传入的是InvocationHandler,可以确定的是这是我们之前定义的那个静态内部类,我们再看代理类执行sellBiotherm的方法,可以看出它直接调用的super.h.invoke().这个h即是之前我们定义的invocationHandler,所以代理类执行方法就直接进入我们定义的invoke方法了。

    此时回看静态代理缺点

    ①因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类产生。
    ②一旦接口增加方法,目标对象与代理对象都需要维护。
    动态代理都能解决。①中对于动态代理来说,它是无需接口的。
    ②中我们举例说明
    给接口添加一个新的方法

    interface SellPorduct{
        void sellBiotherm();
        void sellOthers();
    }
    
    class OverseasStore implements SellPorduct{
        @Override
        public void sellBiotherm() {
            System.out.println("卖出洗面奶——此消息来自供应商");
        }
    
        @Override
        public void sellOthers() {
            System.out.println("卖出其他东西——此消息来自供应商");
        }
    }
    

    代理类不用修改(记得把刚刚测试invocationHandler的代码还原)
    在mian方法中调用该方法

    public class AppStaticTest {
        public static void main(String[] args) {
            OverseasStore overseasStore = new OverseasStore();
            DynamicProxy dynamicProxy = new DynamicProxy();
            dynamicProxy.setTarget(overseasStore);
            SellPorduct proxyInstance = (SellPorduct) dynamicProxy.getProxyInstance();
            proxyInstance.sellBiotherm();
            proxyInstance.sellOthers();
        }
    }
    

    得到收货地址
    卖出洗面奶——此消息来自供应商
    快递已发出
    得到收货地址
    卖出其他东西——此消息来自供应商
    快递已发出
    可以看出jdk的动态代理,必须的要求被代理对象至少实现一个接口,这是一种限制,当然我们也可以看一下cglib代理。它对被代理类没有任何要求。

    Cglib动态代理

    同样还是先看一下代码
    ①真实角色类

    class OverseasStore{
        public void sellBiotherm() {
    
            System.out.println("卖出洗面奶——此消息来自供应商");
        }
        public void sellOthers() {
            System.out.println("卖出其他东西——此消息来自供应商");
        }
    }
    

    ②代理类

    class CglibProxy implements MethodInterceptor{
    
        private Object target;
    
        public CglibProxy(Object target){
            this.target = target;
        }
    
        public Object getInstance(){
            //1.创建一个工具类
            Enhancer enhancer = new Enhancer();
            //2.设置父类
            enhancer.setSuperclass(target.getClass());
            //3.设置回调函数
            enhancer.setCallback(this);
            //创建子类对象,即代理对象
            return enhancer.create();
        }
    
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            System.out.println("cglib开始代理:....");
            method.invoke(target,objects);
            System.out.println("cglib结束代理: ....");
            return null;
        }
    }
    

    执行main方法

    public class CglibTest {
        public static void main(String[] args) {
            OverseasStore overseasStore = new OverseasStore();
            CglibProxy cglibProxy = new CglibProxy(overseasStore);
            OverseasStore instance = (OverseasStore) cglibProxy.getInstance();
            instance.sellBiotherm();
        }
    }
    

    cglib开始代理:....
    卖出洗面奶——此消息来自供应商
    cglib结束代理: ....
    可以看出cglib跟jdk动态代理其实差不太多,cglib通过Enhancer创建一个目标类的子类,这样就可以调用父类的方法了,所以它不需要接口。而接口MethodInterceptor跟接口InvocationHandler基本完全一致。cglib也是在内存中直接创建二进制的字节码,通过asm工具类进行操作。熟悉cglib的都知道,在导入cglib包时,同时还需要导入asm相关包。

    相关文章

      网友评论

          本文标题:设计模式——代理模式

          本文链接:https://www.haomeiwen.com/subject/kzvksltx.html