zoukankan      html  css  js  c++  java
  • java静态代理与动态代理简单分析

    代理这个词大家肯定已经非常熟悉,因为现实中接触的很多,其实现实中的东西恰恰可以非常形象和直观地反映出模式的抽象过程以及本质。现在房子不是吵得热火朝天吗?我们就以房子为例,来拨开代理的面纱。

    假设你有一套房子要卖,一种方法是你直接去网上发布出售信息,然后直接带要买房子的人来看房子、过户等一直到房子卖出去,但是可能你很忙,你没有时间去处理这些事情,所以你可以去找中介,让中介帮你处理这些琐碎事情,中介实际上就是你的代理。本来是你要做的事情,现在中介帮助你一一处理,对于买方来说跟你直接交易跟同中介直接交易没有任何差异,买方甚至可能觉察不到你的存在,这实际上就是代理的一个最大好处。

    接下来我们再深入考虑一下为什么你不直接买房子而需要中介?其实一个问题恰恰解答了什么时候该用代理模式的问题。

    原因一:你可能在外地上班,买房子的人没法找到你直接交易。

    对应到我们程序设计的时候就是:客户端无法直接操作实际对象。那么为什么无法直接操作?一种情况是你需要调用的对象在另外一台机器上,你需要跨越网络才能访问,如果让你直接coding去调用,你需要处理网络连接、处理打包、解包等等非常复杂的步骤,所以为了简化客户端的处理,我们使用代理模式,在客户端建立一个远程对象的代理,客户端就象调用本地对象一样调用该代理,再由代理去跟实际对象联系,对于客户端来说可能根本没有感觉到调用的东西在网络另外一端,这实际上就是Web Service的工作原理。另一种情况虽然你所要调用的对象就在本地,但是由于调用非常耗时,你怕影响你正常的操作,所以特意找个代理来处理这种耗时情况,一个最容易理解的就是Word里面装了很大一张图片,在word被打开的时候我们肯定要加载里面的内容一起打开,但是如果等加载完这个大图片再打开Word用户等得可能早已经跳脚了,所以我们可以为这个图片设置一个代理,让代理慢慢打开这个图片而不影响Word本来的打开的功能。申明一下我只是猜可能Word是这么做的,具体到底怎么做的,俺也不知道。

    原因二:你不知道怎么办过户手续,或者说除了你现在会干的事情外,还需要做其他的事情才能达成目的。

    对应到我们程序设计的时候就是:除了当前类能够提供的功能外,我们还需要补充一些其他功能。最容易想到的情况就是权限过滤,我有一个类做某项业务,但是由于安全原因只有某些用户才可以调用这个类,此时我们就可以做一个该类的代理类,要求所有请求必须通过该代理类,由该代理类做权限判断,如果安全则调用实际类的业务开始处理。可能有人说为什么我要多加个代理类?我只需要在原来类的方法里面加上权限过滤不就完了吗?在程序设计中有一个类的单一性原则问题,这个原则很简单,就是每个类的功能尽可能单一。为什么要单一,因为只有功能单一这个类被改动的可能性才会最小,就拿刚才的例子来说,如果你将权限判断放在当前类里面,当前这个类就既要负责自己本身业务逻辑、又要负责权限判断,那么就有两个导致该类变化的原因,现在如果权限规则一旦变化,这个类就必需得改,显然这不是一个好的设计。

    1、动态代理(Dynamic Proxy)
      代理分为静态代理和动态代理,静态代理是在编译时就将接口、实现类、代理类一股脑儿全部手动完成,但如果我们需要很多的代理,每一个都这么手动的去创建实属浪费时间,而且会有大量的重复代码,此时我们就可以采用动态代理,动态代理可以在程序运行期间根据需要动态的创建代理类及其实例,来完成具体的功能。
      其实方法直接调用就可以完成功能,为什么还要加个代理呢?
      原因是采用代理模式可以有效的将具体的实现与调用方进行解耦,通过面向接口进行编码完全将具体的实现隐藏在内部。


    2、代理实现的一般模式
      其实代理的一般模式就是静态代理的实现模式:首先创建一个接口(JDK代理都是面向接口的),然后创建具体实现类来实现这个接口,在创建一个代理类同样实现这个接口,不同之处在于,具体实现类的方法中需要将接口中定义的方法的业务逻辑功能实现,而代理类中的方法只要调用具体类中的对应方法即可,这样我们在需要使用接口中的某个方法的功能时直接调用代理类的方法即可,将具体的实现类隐藏在底层。
      第一步:定义总接口Iuser.java

     package ceshi1;
     public interface Iuser {
         void eat(String s);
     }

           第二步:创建具体实现类UserImpl.java

    package ceshi1;
    public class UserImpl implements Iuser {
      @Override
      public void eat(String s) {
        System.out.println("我要吃"+s);
      }
    }

    第三步:创建代理类UserProxy.java

    package ceshi1;
    public class UserProxy implements Iuser {
      private Iuser user = new UserImpl();
      @Override
      public void eat(String s) {
        System.out.println("静态代理前置内容");
        user.eat(s);
        System.out.println("静态代理后置内容");
      }
    }

    第四步:创建测试类ProxyTest.java

    package ceshi1;
    public class ProxyTest {
      public static void main(String[] args) {    
        UserProxy proxy = new UserProxy();
        proxy.eat("苹果");
      }
    }

    3、JDK动态代理的实现
      JDK动态代理的思维模式与之前的一般模式是一样的,也是面向接口进行编码,创建代理类将具体类隐藏解耦,不同之处在于代理类的创建时机不同,动态代理需要在运行时因需实时创建。
      第一步:定义总接口Iuser.java

     package ceshi1;
     public interface Iuser {
         void eat(String s);
     }

    第二步:创建具体实现类UserImpl.java

    package ceshi1;
    public class UserImpl implements Iuser {
      @Override
      public void eat(String s) {
        System.out.println("我要吃"+s);
      }
    }

    第三步:创建实现InvocationHandler接口的代理类

    package ceshi1;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    public class DynamicProxy implements InvocationHandler {
      private Object object;//用于接收具体实现类的实例对象
      //使用带参数的构造器来传递具体实现类的对象
      public DynamicProxy(Object obj){
        this.object = obj;
      }
      @Override
      public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
        System.out.println("前置内容");
        method.invoke(object, args);
        System.out.println("后置内容");
        return null;
      }
    }

    第四步:创建测试类ProxyTest.java

    package ceshi1;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Proxy;
    public class ProxyTest {
      public static void main(String[] args) {
        Iuser user = new UserImpl();
        InvocationHandler h = new DynamicProxy(user);
        Iuser proxy = (Iuser) Proxy.newProxyInstance(Iuser.class.getClassLoader(), new Class[]{Iuser.class}, h);
        proxy.eat("苹果");
      }
    }

    4、通过上面的动态代理实例我们来仔细分析研究一下动态代理的实现过程
    (1)首先我要说的就是接口,为什么JDK的动态代理是基本接口实现的呢?
      因为通过使用接口指向实现类的实例的多态实现方式,可以有效的将具体的实现与调用之间解耦,便于后期修改与维护。
    再具体的说就是我们在代理类中创建一个私有成员变量(private修饰),使用接口来指向实现类的对象(纯种的多态体现,向上转型的体现),然后在该代理类中的方法中使用这个创建的实例来调用实现类中的相应方法来完成业务逻辑功能。
    这么说起来,我之前说的“将具体实现类完全隐藏”就不怎么正确了,可以改成,将具体实现类的细节向调用方完全隐藏(调用方调用的是代理类中的方法,而不是实现类中的方法)。
      这就是面向接口编程,利用java的多态特性,实现程序代码的解耦。
    (2)创建代理类的过程
      如果你了解静态代理,那么你会发现动态代理的实现其实与静态代理类似,都需要创建代理类,但是不同之处也很明显,创建方式不同!
      不同之处体现在静态代理我们知根知底,我们知道要对哪个接口、哪个实现类来创建代理类,所以我们在编译前就直接实现与实现类相同的接口,直接在实现的方法中调用实现类中的相应(同名)方法即可;而动态代理不同,我们不知道它什么时候创建,也不知道要创建针对哪个接口、实现类的代理类(因为它是在运行时因需实时创建的)。
      虽然二者创建时机不同,创建方式也不相同,但是原理是相同的,不同之处仅仅是:静态代理可以直接编码创建,而动态代理是利用反射机制来抽象出代理类的创建过程。
      让我们来分析一下之前的代码来验证一下上面的说辞:
        第一点:静态代理需要实现与实现类相同的接口,而动态代理需要实现的是固定的Java提供的内置接口(一种专门提供来创建动态代理的接口)InvocationHandler接口,因为java在接口中提供了一个可以被自动调用的方法invoke,这个之后再说。
        第二点:private Object object;
            public UserProxy(Object obj){this.object = obj;}
      这几行代码与静态代理之中在代理类中定义的接口指向具体实现类的实例的代码异曲同工,通过这个构造器可以创建代理类的实例,创建的同时还能将具体实现类的实例与之绑定(object指的就是实现类的实例,这个实例需要在测试类中创建并作为参数来创建代理类的实例),实现了静态代理类中private Iuser user = new UserImpl();一行代码的作用相近,这里为什么不是相同,而是相近呢,主要就是因为静态代理的那句代码中包含的实现类的实例的创建,而动态代理中实现类的创建需要在测试类中完成,所以此处是相近。
        第三点:invoke(Object proxy, Method method, Object[] args)方法,该方法是InvocationHandler接口中定义的唯一方法,该方法在调用指定的具体方法时会自动调用。其参数为:代理实例、调用的方法、方法的参数列表
      在这个方法中我们定义了几乎和静态代理相同的内容,仅仅是在方法的调用上不同,不同的原因与之前分析的一样(创建时机的不同,创建的方式的不同,即反射),Method类是反射机制中一个重要的类,用于封装方法,该类中有一个方法那就是invoke(Object object,Object...args)方法,其参数分别表示:所调用方法所属的类的对象和方法的参数列表,这里的参数列表正是从测试类中传递到代理类中的invoke方法三个参数中最后一个参数(调用方法的参数列表)中,在传递到method的invoke方法中的第二个参数中的(此处有点啰嗦)。
        第四点:测试类中的异同
      静态代理中我们测试类中直接创建代理类的对象,使用代理类的对象来调用其方法即可,若是别的接口(这里指的是别的调用方)要调用Iuser的方法,也可以使用此法
    动态代理中要复杂的多,首先我们要将之前提到的实现类的实例创建(补充完整),然后利用这个实例作为参数,调用代理来的带参构造器来创建“代理类实例对象”,这里加引号的原因是因为它并不是真正的代理类的实例对象,而是创建真正代理类实例的一个参数,这个实现了InvocationHandler接口的类严格意义上来说并不是代理类,我们可以将其看作是创建代理类的必备中间环节,这是一个调用处理器,也就是处理方法调用的一个类,不是真正意义上的代理类,可以这么说:创建一个方法调用处理器实例。
      下面才是真正的代理类实例的创建,之前创建的”代理类实例对象“仅仅是一个参数
        Iuser proxy = (Iuser) Proxy.newProxyInstance(Iuser.class.getClassLoader(), new Class[]{Iuser.class}, h);
      这里使用了动态代理所依赖的第二个重要类Proxy,此处使用了其静态方法来创建一个代理实例,其参数分别是:类加载器(可为父类的类加载器)、接口数组、方法调用处理器实例
      这里同样使用了多态,使用接口指向代理类的实例,最后会用该实例来进行具体方法的调用即可。

    (3)InvocationHandler

      InvocationHandler是JDK中提供的专门用于实现基于接口的动态代理的接口,主要用于进行方法调用模块,而代理类和实例的生成需要借助Proxy类完成。

      每个代理类的实例的调用处理器都是实现该接口实现的,而且是必备的,即每个动态代理实例的实现都必须拥有实现该接口的调用处理器,也可以这么说,每个动态代理实例都对应一个调用处理器。

      这里要区分两个概念,代理类和代理实例,调用处理器是在创建代理实例的时候才与其关联起来的,所以它与代理实例是一一对应的,而不是代理类。

    (4)Proxy

      Proxy类是JDK提供的用于生成动态代理类和其实例的类。

      我们可以通过Proxy中的静态方法getProxyClass来生成代理类,需要的参数为类加载器和接口列表(数组),然后再通过反射调用代理类的构造器来生成代理实例,需要以一个InvocationHandler作为参数(体现出方法调用是与实例相关的,而非类)。

    InvocationHandler handler = new MyInvocationHandler(...);
    Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class);
    Foo f = (Foo) proxyClass.getConstructor(InvocationHandler.class).newInstance(handler);

    (5)、总结

      我们总结下JDK动态代理的实现步骤:

        第一步:创建接口,JDK动态代理基于接口实现,所以接口必不可少(准备工作)

        第二步:实现InvocationHandler接口,重写invoke方法(准备工作)

        第三步:调用Proxy的静态方法newProxyInstance方法生成代理实例(生成实例时需要提供类加载器,我们可以使用接口类的加载器即可)

        第四步:使用新生成的代理实例调用某个方法实现功能。

      我们的动态代理实现过程中根本没有涉及到真实类实例。

    5、Cglib动态代理的实现

      JDK动态代理拥有局限性,那就是必须面向接口编程,没有接口就无法实现代理,我们也不可能为了代理而为每个需要实现代理的类强行添加毫无意义的接口,这时我们需要Cglib,这种依靠继承来实现动态代理的方式,不再要求我们必须要有接口。

      第一步:添加Cglib的Maven依赖

           第二步:创建具体实现类User.java

           第三步:创建实现MethodInterceptor接口的代理类

    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;
    
    import java.lang.reflect.Method;
    
    public class UserInterceptor implements MethodInterceptor {
    
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            System.out.println("预处理");
            Object object =  methodProxy.invokeSuper(o,objects);
            System.out.println("后处理");
            return object;
        }
    
    }

          第四步:创建测试类ProxyTest.java     

    import net.sf.cglib.proxy.Enhancer;
    
    public class ProxyTest {
        public static void main(String[] args){
            Enhancer enchancer = new Enhancer();//字节码增强器
            enchancer.setSuperclass(User.class);//设置被代理类为父类
            enchancer.setCallback(new UserInterceptor());//设置回调
            User user = (User)enchancer.create();//创建代理实例
            user.eat("葡萄");
        }
    }
  • 相关阅读:
    Macbook上如何把中文文本转成UTF-8
    防火墙学习笔记
    百万长连接并发的限制因素
    meminfo中的字段都是什么意思?
    维护TCP长连接时系统配置的参数net.ipv4.tcp_keepalive_time不生效
    小程序如何利用CSS画出箭头
    cephfs配额问题
    ceph机房断电之后重启遇到问题“Transaction order is cyclic. See system logs for details.”
    valgrind的使用方法
    程序员的创业经历
  • 原文地址:https://www.cnblogs.com/yn-huang/p/10681987.html
Copyright © 2011-2022 走看看