zoukankan      html  css  js  c++  java
  • 用大白话讲Java动态代理的原理

    动态代理是什么

    首先说下代理模式,代理模式是常见的一种java设计模式,特征是代理类委托类实现了同样的接口,代理类主要负责为委托类预处理、过滤、转发,以及事后处理等。代理类与委托类之间通常会存在关联关系,一个代理类的实例与它的委托类的实例是关联的。代理类的实例本身是并不真正关心被调用方法的内部逻辑,而是会通过内部访问调用 委托类的实例真正实现了的方法,来为调用者提供服务。

    有代理的话,在访问实际对象时,是通过代理实例来访问、调用委托类方法的,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。

    动态代理对比静态代理,最大的特点是代理类是在程序运行时生成的,并非在编译期生成,能做的事情也多了,自然风险也高了。

    动态代理最简单的用法

    用一个比较接近生活的例子:中午,饿了的室友 委托 持家有道的你 去点外卖

    Hungry.java :接口

    public interface Hungry {
         void callLunch();
    }
    

    Roommate.java :Hungry接口的实现类,也就是委托类

    public class Roommate implements Hungry{
        private String name;
        public Roommate(String name) {
            this.name = name;
        }
    
        @Override
        public void callLunch() {
            System.out.println("好饿,今天午饭点外卖吧");
        }
    }
    
    
    public class RoommateInvocationHandler<T> implements InvocationHandler {
    
        private T rommate;
    
        public RoommateInvocationHandler(T roommate){
            this.rommate = roommate;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("下单前,我先帮你看下有没有平台优惠券吧");
            Object result = method.invoke(rommate , args);
            return result;
        }
    }
    

    InvocationHandler是一个接口,由代理实例内部的invocation handler实现的接口。每个代理实例都有一个关联的invocation handler。当代理实例上调用方法时,method.invoke(baseImpl, args),此方法将被编码并织入到代理实例内部的 invocation handler实现的invoke方法中。

    利用 Proxy 的方式实现动态代理,调用 委托类接口 的方法,完成午餐点外卖这个操作

    public static void main(String[] args) {
    	    Roommate roommate = new Roommate("zhangsan");
            Hungry proxyInstance = (Hungry) Proxy.newProxyInstance(
                    roommate.getClass().getClassLoader(),
                    roommate.getClass().getInterfaces(),
                    new RoommateInvocationHandler<Roommate>(roommate)
            );
            proxyInstance.callLunch();
    }
    //输出结果
    下单前,我先帮你看下有没有平台优惠券吧
    好饿,今天午饭点外卖吧
    

    代理实例proxyInstance的类型是Hungry,所以只能调用Hungry里规定的方法。Roommate作为接口实现类,不是来自接口的其他的方法,是无法通过动态代理调用的。

    可以看到代理实例在调用委托类实现的方法时,可以很方便地在调用方法的前后执行一些操作,在示例代码中则是在调用方法前简单输出了一行: System.out.println("下单前,我先帮你看下有没有平台优惠券吧"),还可以有其他用途,例如记录这个方法的耗时时间,对方法的参数或者返回结果进行修改等等。这也是Spring,Dagger进行AOP编程的原理。

    那为什么继承InvocationHandler接口和持有委托类引用的RoommateInvocationHandler调用来自Hungry接口的callLunch()方法时可以调用到委托类对callLunch()的逻辑实现呢,看看它的背后原理:

    动态代理的实现原理

    Proxy.newProxyInstance() 入手,逐步分析 InvocationHandler 如何建立代理实例和委托实例的关联:

    public static Object newProxyInstance(ClassLoader loader ,  Class<?>[] interfaces,
                                   InvocationHandler h) throws IllegalArgumentException {
        	//InvocationHandler必须非空,说明是个重要角色
            Objects.requireNonNull(h);
    		//获取委托类的接口
            final Class<?>[] intfs = interfaces.clone();
            final SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
            }
    
            /*
             * Look up or generate the designated proxy class.
             * 核心:通过类加载器和委托类接口,在内存中查找出或者生成指定的代理类
             */
            Class<?> cl = getProxyClass0(loader, intfs);
    
            /*
             * Invoke its constructor with the designated invocation handler.
             * 利用指定的invocation handler调用它的构造器方法,构建代理类的实例返回
             */
            try {
                if (sm != null) {
                    checkNewProxyPermission(Reflection.getCallerClass(), cl);
                }
    
                final Constructor<?> cons = cl.getConstructor(constructorParams);
                final InvocationHandler ih = h;
                if (!Modifier.isPublic(cl.getModifiers())) {
                    AccessController.doPrivileged(new PrivilegedAction<Void>() {
                        public Void run() {
                            cons.setAccessible(true);
                            return null;
                        }
                    });
                }
                return cons.newInstance(new Object[]{h});
            } catch (IllegalAccessException|InstantiationException e) {
                throw new InternalError(e.toString(), e);
            } catch (InvocationTargetException e) {
                Throwable t = e.getCause();
                if (t instanceof RuntimeException) {
                    throw (RuntimeException) t;
                } else {
                    throw new InternalError(t.toString(), t);
                }
            } catch (NoSuchMethodException e) {
                throw new InternalError(e.toString(), e);
            }
        }
    

    来到这一步好像就停下了,那么接下来探究 cl 这个实例创建过程发生了什么:

    在上面示例代码main函数的后面接着补充。利用ProxyGenerator.generateProxyClass生成这个动态生成的类文件,写入了指定路径的class文件内

    $Proxy0 是 代理类在系统内部的编号,在示例代码只生成了一个代理类所以编号是 $Proxy0

            byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0",Roommate.class.getInterfaces());
            String filePath = "C:\Users\ODM\Desktop\RoommateProxy.class";
            try(FileOutputStream fos = new FileOutputStream(filePath)) {
                fos.write(classFile);
                fos.flush();
            }catch (IOException e){
                e.printStackTrace();
                System.out.println("error:写入文件");
            }
    

    使用反编译工具,我这里用的是jd-gui反编译,这个$Proxy0类,实现了Proxy类,继承了和委托类相同的接口

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.reflect.UndeclaredThrowableException;
    import proxy_test.Hungry;
    
    public final class $Proxy0 extends Proxy implements Hungry{
      private static Method m1;
      private static Method m3; //由下方静态代码块得知,m3代表callLunch()这一个方法
      private static Method m2;
      private static Method m0;
      
      /*
      * 父类Proxy的构造器,其中 h 属性为 InvocationHandler引用
      * protected Proxy(InvocationHandler h) {
            Objects.requireNonNull(h);
            this.h = h;
        }
      */
      public $Proxy0(InvocationHandler paramInvocationHandler) throws {
        super(paramInvocationHandler);
      }
     //关键!可供外界调用,方法名与委托类实现接口的方法相同,利用 InvocationHandler调用invoke
      public final void callLunch() throws {
        try{
          this.h.invoke(this, m3, null);
          return;
        }
        catch (Error|RuntimeException localError){
          throw localError;
        }
        catch (Throwable localThrowable){
          throw new UndeclaredThrowableException(localThrowable);
        }
      }
        
      public final boolean equals(Object paramObject) throws {}
      public final String toString() throws {...}
      public final int hashCode() throws {...}
      
      static{
        try{
          m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
          m3 = Class.forName("proxy_test.Hungry").getMethod("callLunch", new Class[0]);
          m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
          m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
          return;
        }
        catch (NoSuchMethodException localNoSuchMethodException){
          throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
        }
        catch (ClassNotFoundException localClassNotFoundException){
          throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
        }
      }
    }
    

    事情逐渐明朗起来,从这个动态类的源码,可以分析出: $Proxy0 ,在构建这个类时,会调用了父类Proxy的构造方法,将InvocationHandler引用传递给了父类Proxy的 h 属性,于是当我们在外界使用 代理实例 调用了 callLunch() 这个方法时,就会来到这一句 this.h.invoke(this, m3, null); 由于h属性其实是InvocationHandler引用,调用了它的invoke,也就导致了上面示例代码中的RoommateInvocationHandler类的重写过的invoke方法也就被调用了,RoommateInvocationHandler也持有委托类的引用,所以委托类的方法也被调用起来了。

    Java的继承机制是单继承,多接口。代理类因为必须要继承Proxy类,所以java的动态代理只能对接口进行代理,无法对一个class类进行动态代理。

    动态代理原理总结

    用大白话的方式讲:

    有一个类InvocationHandler,它的性质类似一个中介,中介类构建时持有了委托对象,所以可以在它的invoke方法中调用了委托对象实现接口的具体方法。当外部调用这个InvocationHandler的invoke方法时,对 invoke 的调用最终都转为对委托对象的方法调用。

    创建明面上负责代理的代理实例时,在内存中动态生成的类不但继承了Proxy,也实现了与委托对象相同的接口,因此代理实例可以调用此接口的方法,然后通过持有的中介类对象来调用中介类对象的invoke方法,最终达到代理实例执行了委托者的方法。

  • 相关阅读:
    Cocos2d-x学习之---自定义图标(带触摸事件)
    Cocos2d-x关于ScrollView
    学习实战三:基于Cocos2d-x引擎模仿微信打飞机游戏
    补算法相关知识一:蚂蚁算法
    避免Cocos2d-x编写的游戏在用eclipse生成安卓包时繁琐的写Android.mk文件
    Cocos2d-x学习之---模仿微信打飞机游戏敌机层设计初想
    Cocos2d-x学习之---2013年10月11日小记
    有时候真怕,时间会说出真心话。
    NO2:设置RedHat Linux下的samba开机启动
    NO1:在Windows端安装SecureCRT来连接Linux
  • 原文地址:https://www.cnblogs.com/DMingO/p/13395530.html
Copyright © 2011-2022 走看看