• JDK 动态代理实现原理


    一、引言

    Java动态代理机制的出现,使得Java开发人员不用手工编写代理类,只要简单地指定一组接口及委托类对象便能动态生成代理类。代理类会负责将所有方法的调用分派到委托对象上反射执行,在分派执行的过程中,开发人员还可以按需调整委托类对象及其功能。

    本文首先从JDK动态代理的允许机制和特点出发,对其代码进行分析,以及推演动态生成类的内部实现。相关概念:代理类委托类调用处理器等。

    二、相关类和接口

    1、java.lang.reflect.Proxy:提供了一组静态方法为一组接口动态的生成代理类及其对象。

    // 方法1:该方法用于获取指定代理类对象所关联的调用处理器
    public static InvocationHandler getInvocationHandler(Object proxy)
    
    // 方法2:该方法用于获取指定类装载器和一组接口的动态代理类对象
    public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
    
    // 方法3:该方法用于判断指定类对象是否是一个动态代理类
    public static boolean isProxyClass(Class<?> cl)
    
    // 方法4:该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

    2、java.lang.reflect.InvocationHandler:调用处理器接口,定义了一个invoke方法,用于集中处理【动态代理类对象】上的方法调用,通常在该方法中实现对委托类的代理访问。

    3、java.lang.ClassLoader:类装载器,负责将类的字节码装载到Java虚拟机(JVM)中并为其定义类对象,然后该类才能被调用。Proxy静态方法生成【动态代理类】同样需要通过类装载器进行装载后才能使用,它与普通类的唯一区别就是其字节码是由JVM在运行时动态生成,而非预存在任何一个.class文件中

    三、实现步骤及其特点

    1、实现步骤:一共四步,具体如下

    • 1)创建调用处理器(通过实现InvocationHandler接口)
    • 2)创建动态代理类(通过为Proxy类指定ClassLoader对象和一组interface)
    • 3)获得动态代理类的构造函数(通过反射,其唯一参数类型是调用处理器接口类型)
    • 4)创建动态代理类的实例(通过步骤3得到的构造函数,传入调用处理器对象作为参数)
    // MyInvocationHandler实现了InvocationHandler接口,并能实现方法调用从代理类到委托类的分派转发
    // 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用
    InvocationHandler handler = new MyInvocationHandler(...);
    
    // 通过Proxy创建动态代理类
    Class proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), new Class[] { Foo.class });
    
    // 通过反射获得动态代理类的构造函数
    Constructor constructor = proxyClass.getConstructor(new Class[] { InvocationHandler.class });
    
    // 通过构造函数创建动态代理类实例
    Foo f = (Foo) constructor.newInstance(new Object[] { handler });

     实际使用过程更加简单,因为Proxy的静态方法newProxyInstance已经封装了步骤2至步骤4,所以简化后的过程如下

    InvocationHandler handler = new MyInvocationHandler(...);
    
    // 通过Proxy直接创建动态代理类实例
    Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class[] { Foo.class }, handler);

    2、代理类的特点

    • 1)若所代理的接口中有非public接口则动态代理类将被定义在该接口所在包,否则将被定义在顶层包。目的是为了最大程度保证动态代理类不会因为包管理的问题而无法被成功定义并访问
    • 2)该代理类具有final和public修饰符,意味着它可以被所有类访问,但是不能被再度继承。
    • 3)类名格式是“$ProxyN”,其中N是一个逐一递增的阿拉伯数据,代表Proxy类第N次生成的动态代理类。值得注意的一点是,并不是每次调用Proxy的静态方法创建动态代理类都会是N值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会返回先前已经创建好的代理类对象,提高了创建效率
    • 4)类继承关系:Proxy类是它的父类,且该类实现了其所代理的一组接口,这就是为什么它能够被安全的类型转换到其所代理的某接口的根本原因。

    3、代理类实例的特点

    • 1)每个实例都会关联一个调用处理器对象,可以通过Proxy提供的静态方法getInvocationHandler获取。
    • 2)调用其代理接口中声明的方法时,最终都会由调用处理器的invoke方法执行,包括hashCode、equals、toString。
    • 3)当代理的一组接口有重复声明的方法且该方法被调用时,代理类总是从排在最前面的接口获取方法对象并分派给调用处理器,因为在代理类内部无法区分其当前的被引用类型

    4、被代理接口的特点

    • 1)不能有重复的接口,以避免动态代理类代码生成时的编译错误
    • 2)这些接口对类装载器必须可见,否则会导致类定义失败
    • 3)被代理的所有非public接口必须在同一个包中,否则也会导致类定义失败,原因见【代理类的特点1】
    • 4)接口的数目不能超过65535(2的16次方-1),这是JVM设定的限制

    5、异常处理的特点

    • 1)理论上调用处理器可以抛出任何类型的异常,因为所有异常都继承于Throwable接口,但实际上往往受限制,除非父接口中的方法支持抛Throwable异常
    • 2)如果在invoke方法中产生了不支持的异常,它将抛出UndeclaredThrowableException异常,这个异常时RuntimeException类型,所以不会引起编译错误。通过该异常的getCause方法可以获得那个不受支持的异常对象,以便与错误诊断。

    四、Proxy类实现原理

    1、Proxy的重要静态变量

    // 映射表:用于维护类装载器对象到其对应的代理类缓存
    private static Map<ClassLoader, Map<List<String>, Object>> loaderToCache = new WeakHashMap<>();
    
    // 标记:用于标记一个动态代理类正在被创建中
    private static Object pendingGenerationMarker = new Object();
    
    // 同步表:记录已经被创建的动态代理类类型,主要用于方法isProxyClass 进行相关的判断
    private static Map<Class<?>, Void> proxyClasses = Collections.synchronizedMap(new WeakHashMap<Class<?>, Void>());
    
    // 关联的调用处理器引用
    protected InvocationHandler h;

    2、Proxy构造方法

    // 由于 Proxy 内部从不直接调用构造函数,所以 private 类型意味着禁止任何调用
    private Proxy() {} 
    
    // 由于 Proxy 内部从不直接调用构造函数,所以 protected 意味着只有子类可以调用
    protected Proxy(InvocationHandler h) {
      doNewInstanceCheck();
      this.h = h;
    }

     3、Proxy静态方法newProxyInstance,详见jar包

     4、getProxyClass方法详解,四个步骤

    • 1)对接口进行安全检查,包括检查a.是否对类装载器可见(通过Class.forName方法判断),b.确保是interface而不是class类型,c.接口是否重复,检查通过后得到一个包含所有接口名称的字符串数组。
    • 2)从loaderToCache映射表获取以类装载器为key所对应的缓存表(接口列表为key,动态代理类对象为value),如果不存在则创建一个并更新到loaderToCache。当代理类正在被创建时它会临时保存(接口列表,pendingGenerationMarker)。标记pendingGenerationMarker的作用是通知后续的同类请求(接口数组相同且组内接口顺序也相同)代理类正在被创建,请保持等待直至创建完成。
    • 3)动态创建代理类的对象。首先确定代理类所在的包,检查是否所有非public的接口都在同一个包,否则抛异常终止代理类生成;之后开始生成代理类的类名,格式如前所述“$ProxyN”;最后动态生成代理类。
      // 动态地生成代理类的字节码数组
      byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces); 
      try { 
          // 动态地定义新生成的代理类
          proxyClass = defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); 
      } catch (ClassFormatError e) { 
          throw new IllegalArgumentException(e.toString()); 
      } 
      
      // 把生成的代理类记录进 proxyClasses 表
      proxyClasses.put(proxyClass, null);
    • 4)最后根据结果更新缓存表,如果成功则将代理类更新进缓存表,否则清除缓存表对应的key,再唤醒所有正在等待的线程。

    五、代理类中方法调用的分派转发推演实现

    // 假设需代理接口 Simulator 
    public interface Simulator { 
        short simulate(int arg1, long arg2, String arg3) throws ExceptionA, ExceptionB;
    } 
    
    // 假设代理类为 SimulatorProxy, 其类声明将如下
    final public class SimulatorProxy implements Simulator { 
        
        // 调用处理器对象的引用
        protected InvocationHandler handler; 
        
        // 以调用处理器为参数的构造函数
        public SimulatorProxy(InvocationHandler handler){ 
            this.handler = handler; 
        } 
        
        // 实现接口方法 simulate 
        public short simulate(int arg1, long arg2, String arg3) 
            throws ExceptionA, ExceptionB {
    
            // 第一步是获取 simulate 方法的 Method 对象
            java.lang.reflect.Method method = null; 
            try{ 
                method = Simulator.class.getMethod( 
                    "simulate", 
                    new Class[] {int.class, long.class, String.class} );
            } catch(Exception e) { 
                // 异常处理 1(略)
            } 
            
            // 第二步是调用 handler 的 invoke 方法分派转发方法调用
            Object r = null; 
            try { 
                r = handler.invoke(this, 
                    method, 
                    // 对于原始类型参数需要进行装箱操作
                    new Object[] {new Integer(arg1), new Long(arg2), arg3});
            } catch(Throwable e) { 
                // 异常处理 2(略)
            } 
            // 第三步是返回结果(返回类型是原始类型则需要进行拆箱操作)
            return ((Short)r).shortValue();
        } 
    }

    从以上推演中可以得出一个通用的结构化流程:1.从代理接口获取被调用的方法对象,2.分派方法到调用处理器执行,3.返回结果

    六、参考资料

    1、http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/index.html

  • 相关阅读:
    mysql常用函数
    快看看你躺枪了吗?最全搞笑中式英语大集合
    浏览器 返回状态码汇总
    Eclipse 快捷键 篇
    Spring定时任务的几种实现
    mysql ---复制表结构---创建新表
    日志级别的选择:Debug、Info、Warn、Error还是Fatal
    关闭网页时如何弹出消息框 提醒用户:您确认关闭吗 ?
    处理session丢失的问题
    工作中存在的问题
  • 原文地址:https://www.cnblogs.com/orzlin/p/6016212.html
Copyright © 2020-2023  润新知