zoukankan      html  css  js  c++  java
  • 常见的设计模式:代理模式

    1. 什么是代理模式

      笔者在一次面试经历当中,被面试官问到什么是代理模式的时候,自己一脸懵逼的样子,被面试官嫌弃了一番,说这是java当中最常见的设计模式,并且还因此被面试官打上了“小伙子基础不扎实”的标签后,自己痛定思痛,决定好好理解在java当中的代理模式究竟是什么,于是查阅了很多网上大牛写的博客,再次也记录一下自己的理解和学习到的东西。

      那么,究竟什么是代理模式呢?所谓的代理模式,主要分为一下几个角色:1.代理方;2.目标方;3.客户方;为了让读者和将来的自己能够更好的理解这三个角色,举个例子,比如我们要在买一双鞋子,但是鞋店离我们很远,我们需要让一个朋友帮我们代购,那么在这个关系当中,我们就是作为客户方,朋友作为代理方,而鞋店则是目标方,我们需要做的是要讲我们要买的鞋子的需求告诉朋友听,然后朋友帮我们到店里去购买,购买之后还可以为我们的鞋子简单的包装之后再邮寄回我们。这就是代理的三方关系,从这个例子我们可以给出代理的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用, 其特征是代理类与委托类有同样的接口。关系如图:

      在进一步理解这个代理类的作用,代理类除了能帮我们把消息递交给目标类之外,还可以为目标类预处理消息,过滤消息,事后处理消息等。在我们的例子中的体现就是朋友不仅可以帮我们买鞋子,还可以为鞋店提供一些购买请求的过滤,比如我们要买的鞋子如果鞋店没有,朋友会直接告诉我们没有这双鞋,而不需要到鞋店去再问一遍,并且在我们购买到鞋子之后,朋友还会负责之后的邮寄和鞋子的包装,这就是相当于代理类为目标类做的消息预处理,消息过滤,以及事后消息预处理。代理类的对象本身并不真正实现服务,而是通过调用目标类的对象的相关方法,来提供特定的服务,从我们的例子来看,朋友自身是不生产鞋子的,而是鞋店生产的鞋子,并且提供销售,是一个道理。真正的业务是由目标类来实现的,但是在实现目标类之前的一些公共服务,例如在项目开发中我们没有加入缓冲,日志这些功能,后期想加入,我们就可以使用代理来实现,而没有必要打开已经封装好的目标类。

    2.静态代理模式

      所谓的静态代理,指的是由程序员创建或特定工具自动生成的源代码,再对其编译。在程序运行前,代理的类文件就已经存在。即实现程序员已经完成对代理程序的设计。那么如何去实现一个静态代理呢?静态代理的实现步骤大体分为一下几个步骤:

    1.我们需要定义一个业务接口

    2.编写这个业务接口的实现类(目标类)

    3.然后编写一个代理类,也和目标类实现同一个业务接口,代理类需要注入目标类。

    4.最后写一个Client来调用这个业务接口(客户方)

    那么我们先来完成第一个步骤,定义一个业务接口,这个接口我们就延续我们的买鞋子的例子。接口定义如下:

    package wellhold.bjtu.proxy;
    
    public interface BuyShoes {
    
        public void BuyTheShoes();
        
    }

    定义了一个买鞋子的业务接口。

    接下来我们要完成第二个步骤,编写一个业务接口的实现类,即目标类。

    package wellhold.bjtu.proxy;
    
    public class ShoesSeller implements BuyShoes {
    
        @Override
        public void BuyTheShoes() 
        {
            System.out.println("这里是鞋店,您要的鞋子有货");
        }
    }

    定义了一个买鞋子的业务实现类,即目标类,在目标类当中调用买鞋子这个方法自动打印“这里是鞋店,您要的鞋子有货”。

    之后我们继续完成第三个步骤。编写一个代理类,同样也实现买鞋子这个接口。

    package wellhold.bjtu.proxy;
    
    public class ShoesSellerProxy implements BuyShoes {
    
        private ShoesSeller shoesSeller;
        
        public ShoesSellerProxy(ShoesSeller shoesSeller) 
        {
            this.shoesSeller=shoesSeller;
        }
        
        @Override
        public void BuyTheShoes() {
    
            System.out.println("这里是朋友,我需要对你的购买请求做一个判断,你要的鞋子究竟有没有");
            shoesSeller.BuyTheShoes();
            System.out.println("这里是朋友,我已经买到你要的鞋子,现在给你发回去");
        }
    
    }

    定义了一个代理类,通过构造函数可以目标类注入,并且在调用相应方法的时候,可以做一些预处理和事后处理。

    最后我们只需要再编写一个客户方的类去对这个业务接口进行调用即可,代码如下:

    package wellhold.bjtu.proxy;
    
    public class BuyShoesClient {
    
        public static void main(String[] args) {
    
            ShoesSeller ss=new ShoesSeller();
            ShoesSellerProxy ssp=new ShoesSellerProxy(ss);
            ssp.BuyTheShoes();
        }
    
    }

    可以看到,在客户方我们需要做的就是实例化一个目标类和一个代理类,并且将目标类注入到代理类当中,然后我们是通过代理去执行BuyTheShoes这个业务的,可以看看执行的结果:

    可以看到目标类的BuyTheShoes方法被代理类加强了,实现了预处理,和事后处理的逻辑代码块。至此我们的静态代理就实现了。

    3.动态代理

    从静态代理的原理来看,对代理模式而言,一个目标类与一个代理类一一对应,这也是静态代理模式的特点,当然一个代理类也可以实现多个业务接口,去做多个目标类的代理对象,但这个代理类就会需要重写很多业务方法,看起来特别冗杂,所以一般而言一个目标类对应一个代理类,比如我们的朋友除了可以给我们代买鞋子,也可以给我们代买手机,即朋友也可以作为手机店的代理,是一个道理。但也有这种情况,即有N个目标类,但是在代理类当中的预处理和事后处理是一样,仅仅是目标类的不同,则我们就可以使用动态代理模式。从我们的例子说,就是不论你买的是手机,还是鞋子,朋友都需要为先处理你的请求店家是否有,然后再把买到的东西邮寄回来,这两个事前和事后流程是一样的,所以这时候我们可以使用动态代理。

    以我们的例子为背景,那么首先我们做两个业务接口,分别为买鞋子和买手机:

    package wellhold.bjtu.DynamicProxy;
    
    public interface BuyPhone {
    
        public void BuyThePhone();
        
    }
    package wellhold.bjtu.DynamicProxy;
    
    public interface BuyShoes {
    
        public void BuyTheShoes();
        
    }

    然后做两个类,去实现这两个业务接口当中声明的方法。

    package wellhold.bjtu.DynamicProxy;
    
    public class PhoneSeller implements BuyPhone {
    
        @Override
        public void BuyThePhone() 
        {
            System.out.println("这里是手机店,您要的手机有货");
        }
    
    }
    package wellhold.bjtu.DynamicProxy;
    
    public class ShoesSeller implements BuyShoes {
    
        @Override
        public void BuyTheShoes() 
        {
            System.out.println("这里是鞋店,您要的鞋子有货");
        }
    }

    这时候,如果我们用的静态代理的方式去实现代理方可以代理这两个业务方法,则这个代理类除了要实现BuyShoes接口之外,还需要实现BuyPhone接口,我们可以先看一下用静态代理去实现的话,静态代理类的实现方式:

    package wellhold.bjtu.StaticProxy;
    
    public class SellerProxy implements BuyShoes,BuyPhone {
    
        private ShoesSeller shoesSeller;
        private PhoneSeller phoneSeller;
        
        public SellerProxy(ShoesSeller shoesSeller) 
        {
            this.shoesSeller=shoesSeller;
        }
        
        public SellerProxy(PhoneSeller phoneSeller) 
        {
            this.phoneSeller=phoneSeller;
        }
        
        @Override
        public void BuyTheShoes() {
    
            System.out.println("这里是朋友,我需要对你的购买请求做一个判断,你要的鞋子究竟有没有");
            shoesSeller.BuyTheShoes();
            System.out.println("这里是朋友,我已经买到你要的鞋子,现在给你发回去");
        }
    
        @Override
        public void BuyThePhone() {
            System.out.println("这里是朋友,我需要对你的购买请求做一个判断,你要的手机究竟有没有");
            phoneSeller.BuyThePhone();
            System.out.println("这里是朋友,我已经买到你要的手机,现在给你发回去");
            
        }
    
    }

    可以看到这个静态代理类实现了两个接口,并且重写了两个方法,并且我们要做的预处理和事后处理在这两个方法当中是一样的,假设我们有100个接口的话,这个静态代理类就显得很庞大冗杂,并且重复的代码很多,代码利用率也不高,这时候我们开始考虑使用动态代理的方式去实现。

    要想使用动态代理去实现,首先我们要先了解使用动态代理的时候,必须使用的到的接口InvocationHandler和类Proxy,在动态代理类实现了InvocationHandler这个接口之后,会自动重写一个方法,名叫invoke的方法,该方法有三个参数:

    Object invoke(Object proxy, Method method, Object[] args) throws Throwable
    
    proxy:  指代我们所代理的那个目标类
    method:  指代的是我们所要调用目标类的某个方法Method对象
    args:  指代的是调用目标类某个方法时接受的参数

    可以理解成静态代理类当中,对接口方法实现的统一重写,这里不理解没关系,可以在接下来的例子当中更加形象的去理解。

    而在动态代理中,我们的客户方去调用业务接口的方法的方式就和静态代理大不相同了。在静态代理中,客户方是通过实例化静态代理类去调用方法,而在动态代理中,客户方是通过接口的方式去调用的,而这个接口是通过类ProxynewProxyInstance返回的。那么我们重点来关注一下这个Proxy类的newProxyInstance方法:

    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
    
    loader: 一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载,一般情况下是写入动态代理类的实例类加载器的.getClassLoader()方法
    
    interfaces: 一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),一般写入目标类实例的.getInterFace()方法
    
    h: 一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上

    可以看到在这个方法当中,我们需要传入三个参数,这三个参数从备注中可以大体知道是什么含义,但是也是相对比较抽象的,所以现在不了解也没关系,可以通过多应用去加深记忆。

    介绍完InvocationHandler接口和Proxy类之后,我们来看看我们的代码如何实现,首先两个业务接口和业务接口的实现类在之前已经上过代码了,这里就不再重复了,我们主要来看一下我们的动态代理类如何的实现,且看代码:

    package wellhold.bjtu.DynamicProxy;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    
    public class DynamicProxy implements InvocationHandler {
        
        private Object subject;
        
        public DynamicProxy(Object subject) 
        {
            this.subject=subject;
        }
    
        @Override
        public Object invoke(Object arg0, Method arg1, Object[] arg2) throws Throwable {
            System.out.println("事情预处理代码块");
            arg1.invoke(subject, arg2);
            System.out.println("事情时候处理代码块");
            return null;
        }
    
    }

    从代码当中可以看出,这个动态代理类实现了InvocationHandler接口,并且重写了invoke方法,对比静态代理类的代码和动态代理类的代码,可以看到,传入的目标类实例,在动态代理类当中统一使用了Object类型的subject引用变量去引用,目标类的实现方法统一用invoke方法去承载,在invoke方法当中传入目标类,以及要调用的目标类的方法,当代理类的实例调用目标类的方法的时候,会自动跳转到代理对象关联的handler对象的invoke方法来进行调用。

    之后我们看一下客户方的实现方式,客户方的实现方式与静态代理的方式有很大不同:

    package wellhold.bjtu.DynamicProxy;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Proxy;
    
    import wellhold.bjtu.DynamicProxy.*;
    
    public class Client {
    
        public static void main(String[] args) {
    
            //目标类实例
            ShoesSeller shoesSeller = new ShoesSeller();
            PhoneSeller phoneSeller = new PhoneSeller();
    
            //声明一个接口的引用变量去指向动态代理类实例,动态代理类实例与相应的目标类实例关联
            InvocationHandler shoeshandler = new DynamicProxy(shoesSeller);
            InvocationHandler phonehandler = new DynamicProxy(phoneSeller);
            
            /*
             * 通过Proxy的newProxyInstance方法来创建我们的代理对象,我们来看看其三个参数 
             * 第一个参数
             * handler.getClass().getClassLoader()
             * ,我们这里使用handler这个类的ClassLoader对象来加载我们的代理对象
             * 第二个参数
             * realSubject.getClass().getInterfaces(),
             * 我们这里为代理对象提供的接口是真实对象所实行的接口 ,表示我要代理的是该真实对象,这样我就能调用这组接口中的方法了
             * 第三个参数
             * handler
             * 我们这里将这个代理对象关联到了上方的 InvocationHandler 这个对象上
             */
            
            BuyShoes buyTheShoes = (BuyShoes) Proxy.newProxyInstance(shoeshandler.getClass().getClassLoader(),
                    shoesSeller.getClass().getInterfaces(), shoeshandler);
            buyTheShoes.BuyTheShoes();
            
            
            BuyPhone buyThePhone=(BuyPhone)Proxy.newProxyInstance(phonehandler.getClass().getClassLoader(), 
                    phoneSeller.getClass().getInterfaces(),phonehandler);
            buyThePhone.BuyThePhone();
    
    
        }
    
    }

    通过代码,可以看出,我们是通过声明接口的引用变量handler,去关联目标类和动态代理类的,之后再通过Proxy的newproxyInstance方法去返回一个代理实例,并且用业务接口的引用变量去承载,之后通过调用业务接口的方法去实现业务的,可能语言上描述起来有些拗口。这里要注意一点:在java当中,接口是不可以被实例化的,但是允许定义接口类型的引用变量,该引用变量引用实现了这个接口的类的实例,这就好比声明了一个协议,任何实现了这个协议的类型实例都可以被接受这个协议的方法调用。

    然后我们看看我们的动态代理的效果图:

    至此我们的代理模式就说完了,第一遍看,有很多地方自己也都不是很理解,希望之后自己在了解了java的反射机制之后,再回来看这部分,自己会有更深刻的理解。

  • 相关阅读:
    Execution Order of Event Functions
    【转】unity自带寻路Navmesh入门教程(三)
    【转】unity自带寻路Navmesh入门教程(二)
    【转】unity自带寻路Navmesh入门教程(一)
    【转】UGUI EventSystem
    【转】超简单利用UGUI制作圆形小地图
    【转】UGUI(小地图的实现)与游戏关卡选择的简单实现
    【转】UGUI实现unity摇杆
    redis aof持久化遇到的Can't open the append-only file Permissi
    redis aof持久化遇到的Can't open the append-only file Permissi
  • 原文地址:https://www.cnblogs.com/WellHold/p/6645357.html
Copyright © 2011-2022 走看看