zoukankan      html  css  js  c++  java
  • 动态代理(一)——JDK中的动态代理

    在开始动态代理的描述之前,让我们认识下代理。代理:即代替担任执行职务。在面向对象世界中,即寻找另一个对象代理目标对象与调用者交互。Java中分为静态代理和动态代理。这里对于静态代理不做详述。它们之间的区别,即前者是编译时生成代理对象,后者在运行时生成代理对象,体现一静一动。

    一.前言

    Java中实现动态代理的技术非常繁多,目前主流技术是以下三种:

    • JDK动态代理
    • CGLIB
    • Javassist

    JDK动态代理和CGLIB在Spring AOP中有所使用,Javassit在Hibernate中有所使用。当然还有很多其他的动态代理实现技术:ASM、Byte Buddy、JiteScript、Proxetta等等。这里只对JDK动态代理和CGLIB做详细说明。关于其他代理技术的介绍可以参考 Are there alternatives to cglib?

    本篇文章只对JDK动态代理的实现做深入了解,CGLIB原理后续文章再做详细介绍。

    二.实现动态代理的Demo

    创建JDK动态代理可以分为以下几个步骤:

    • 创建被代理对象的接口
    • 创建被代理对象
    • 创建InvocationHandler
    • 创建代理对象并调用
    1.创建被代理对象的接口
    public interface EchoService {
         void echo(String message);
    }
    
    2. 创建被代理对象
    public class EchoServiceImpl implements EchoService {
        @Override
        public void echo(String message) {
            System.out.println(message);
        }
    }
    
    3. 创建InvocationHandler
    public class DefaultCommonHandler implements InvocationHandler {
        private Object object;
    
        public DefaultCommonHandler(Object o) {
            this.object = o;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("before invoke");
            return method.invoke(object, args);
        }
    }
    
    4.创建代理对象
    public class JdkDynamicProxyTest {
    
        public static void main(String[] args) throws IOException {
            InvocationHandler handler = new DefaultCommonHandler(new EchoServiceImpl());
            EchoService proxyObject = (EchoService) Proxy
                .newProxyInstance(EchoService.class.getClassLoader(), new Class[]{EchoService.class}, handler);
            proxyObject.echo("invoking");
        }
    }
    

    上述执行结果:
    before invoke
    invoking

    从以上的编写过程和执行结果中,应该大致的感受到动态代理的运行逻辑,接下来深入理解其原理。

    三.深入认知原理前的基础准备

    在进一步深入认知JDK动态代理原理之前,先来学习下JDK动态代理实现的基础。Java中提供两个api:Proxy类和InvocationHandler调用委派处理器,用于实现JDK动态代理。

    先来看下Proxy的Java docs描述:

    {@code Proxy} provides static methods for creating dynamic proxy classes and instances, and it is also the superclass of all dynamic proxy classes created by those methods

    从描述中可以看出,Proxy有以下特点:

    • Proxy提供了静态方法用于运行时创建代理类的class对象和代理类的实例对象
    • Proxy是所有的动态代理类的超类

    Proxy提供了两种方式创建代理类的对象:

    1.先创建代理的类的class对象,然后通过class对象获取构造函数,通过构造函数对象Constructor创建实例对象
    InvocationHandler handler = new MyInvocationHandler(...);
    Class proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(),
        new Class[] { Foo.class });
    Foo f = (Foo) proxyClass.getConstructor(new Class[] { InvocationHandler.class })
        .newInstance(new Object[] { handler });
    
    2. 通过Proxyy停的newProxyInstance方法直接创建代理类的实例。本质上也是上面的过程。
    Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), 
        new Class[] { Foo.class }, handler);
    

    本篇文章主要目的是介绍动态代理的原理,不是对Proxy的具体实现细节做深入了解。这里不再赘述Proxy的细节。

    再来看下InvocationHandler。InvocationHandler是动态代理实现的关键,代理模式中的扩展点就是由其承担的。代理的逻辑都是从InvocationHandler中的invoke方法实现。首先看下Java docs描述:

    {@code InvocationHandler} is the interface implemented by the invocation handler of a proxy instance.

    Each proxy instance has an associated invocation handler. When a method is invoked on a proxy instance,the method invocation is encoded and dispatched to the {@code invoke} method of its invocation handler

    从描述中得知,InvocationHandler与代理类的实例有关,每个代理类中都持有一个实现InvocationHandler接口的实例。代理类上的方法的调用将被组织和委派到InvocationHandler的统一方法invoke上。

    下面从源码上简单介绍下上述的实现:

    /**
     * the invocation handler for this proxy instance.
     * @serial
     */
    protected InvocationHandler h;
    
    /**
     * Prohibits instantiation.
     */
    private Proxy() {
    }
    
    /**
     * Constructs a new {@code Proxy} instance from a subclass
     * (typically, a dynamic proxy class) with the specified value
     * for its invocation handler.
     *
     * @param   h the invocation handler for this proxy instance
     */
    protected Proxy(InvocationHandler h) {
        doNewInstanceCheck();
        this.h = h;
    }
    

    以上代码片段是Proxy中的成员域和构造方法。Proxy类中持有InvocationHandler实例,且封装访问为protected,并在Proxy的构造器中初始化。前面提到所有的代理类都继承了Proxy类,所以代理类中拥有InvocationHandler实例,并在代理类初始化时调用父类Proxy的构造器对InvocationHandler进行赋值。

    四.分析代理对象中的细节

    如果要分析代理对象的细节,必然需要阅读代理对象中代码实现。

    1. 可以阅读字节码,但是可读性太低且技术门槛过高
    2. 反编译代理对象的.class文件

    以上两种方式都需要获取代理对象的.class字节码文件。Java中提供了参数配置生成代理对象的class文件。
    Java中生成class文件方法是ProxyGenerator,生成代码如下:

    public static byte[] generateProxyClass(final String var0, Class[] var1) {
        ProxyGenerator var2 = new ProxyGenerator(var0, var1);
        final byte[] var3 = var2.generateClassFile();
        if(saveGeneratedFiles) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    try {
                        FileOutputStream var1 = new FileOutputStream(ProxyGenerator.dotToSlash(var0) + ".class");
                        var1.write(var3);
                        var1.close();
                        return null;
                    } catch (IOException var2) {
                        throw new InternalError("I/O exception saving generated file: " + var2);
                    }
                }
            });
        }
        return var3;
    }
    

    主要是if条件中参数决定是否生成代理对象的.class文件:

    private static final boolean saveGeneratedFiles = ((Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"))).booleanValue();
    

    从JDK源代码中可以分析出只要,设置参数sun.misc.ProxyGenerator.saveGeneratedFiles即可。从这里可以看出只要设置即可:

     System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
    

    Boolean类中的getBoolean如下:

    public static boolean getBoolean(String name) {
        boolean result = false;
        try {
            result = toBoolean(System.getProperty(name));
        } catch (IllegalArgumentException e) {
        } catch (NullPointerException e) {
        }
        return result;
    }
    

    从以上生成.class文件的generateProxyClass方法中可以看出,使用的是FileOutputStream文件输出流,文件的输出目录需要预先创建 好,否则将会抛出异常FileNotFoundException

    在准备生成好.class文件后,下面详细分析生成的代理类。生成的代理类类名都是以$Proxy0开头,如:$Proxy0.class。

    如下就是代理类反编译后的代码:

    package com.sun.proxy;
    
    import com.java.proxy.api.EchoService;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.reflect.UndeclaredThrowableException;
    
    public final class $Proxy0 extends Proxy implements EchoService {
    
        private static Method m1;
        private static Method m3;
        private static Method m0;
        private static Method m2;
    
        public $Proxy0(InvocationHandler var1) throws  {
            super(var1);
        }
    
        public final boolean equals(Object var1) throws  {
            try {
                return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue();
            } catch (RuntimeException | Error var3) {
                throw var3;
            } catch (Throwable var4) {
                throw new UndeclaredThrowableException(var4);
            }
        }
    
        public final void echo(String var1) throws  {
            try {
                super.h.invoke(this, m3, new Object[]{var1});
            } catch (RuntimeException | Error var3) {
                throw var3;
            } catch (Throwable var4) {
                throw new UndeclaredThrowableException(var4);
            }
        }
    
        public final int hashCode() throws  {
            try {
                return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue();
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
    
        public final String toString() throws  {
            try {
                return (String)super.h.invoke(this, m2, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
    
        static {
            try {
                m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
                m3 = Class.forName("com.java.proxy.api.EchoService").getMethod("echo", new Class[]{Class.forName("java.lang.String")});
                m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
                m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            } catch (NoSuchMethodException var2) {
                throw new NoSuchMethodError(var2.getMessage());
            } catch (ClassNotFoundException var3) {
                throw new NoClassDefFoundError(var3.getMessage());
            }
        }
    }
    

    往往探知究竟还是需要阅读源码。这里我们使用这种看起来土但是却是无比简单易用的方式来分析JDK生成的代理类的细节。

    这里最让人关注无非就是动态代理类的调用时怎样委派到最终实际的被代理对象上的这个过程的。在阅读source前,先直观的感受下调用过程图:

    从图中看出,可以将整个调用过程分为几个部分:

    • 调用者对象
    • 代理对象
    • 实现invocationHandler对象
    • 被代理对象

    调用者invoke代理对象,代理对象进行组织和委派给InvocationHandler的统一接口invoke方法,invocationHandler中invoke通过反射的方式调用被代理对象的方法。

    下面再来分析下动态生成的代理类的基本特点:

    • 代理类集成Proxy类且实现创建代理对象时的列表中的接口
    • 代理类是final,不能再被继承
    • 代理类中持有InvcationHandler的实例
    • 代理类对Object中的toString、hashCode、equals方法进行了代理,对构造时的接口中的所有方法进行了代理
    • 代理使用静态语句块,利用反射获取了这些方法的描述对象Method。在创建代理类实例时就已经将这些方法解析,toString、hashCode、equals一般是前三个方法m0、m1、m2
    • 代理类中生成的代理方法都是final
    • 代理类不会对被代理对象中的final,private,protected方法进行代理
    • 代理类名以$Proxy开头

    上面提到InvocationHandler的统一接口invoke,这个是至关重要的点。代理对象到别代理对象的过渡就是通过这个方法委派,下面看下这个接口的定义:

    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
    

    第一个参数是代理对象,第二个参数是调用者调用的被代理的代理方法,第三个参数是被代理方法的参数。

    如果代理的所有接口中有重复方法,则取第一个方法。

    参考

    Dynamic Proxy Classes
    dynamic-proxy
    Are there alternatives to cglib?

  • 相关阅读:
    bootstrap不同屏幕区分数值
    jq星星评分
    大話西遊
    HDU 5353 Average 贪心
    HDU 5358 First One 数学+尺取法
    生活感受
    HDU 4372 Count the Buildings 组合数学
    暑假集训-合训第九场
    一些资料
    多校-HDU 5351 MZL's Border 数学规律
  • 原文地址:https://www.cnblogs.com/lxyit/p/9272319.html
Copyright © 2011-2022 走看看