关于动态代理:浅谈JDK动态代理转载2

9次阅读

共计 12411 个字符,预计需要花费 32 分钟才能阅读完成。

前情提要

假如当初项目经理有一个需要:在我的项目现有所有类的办法前后打印日志。

你如何在 不批改已有代码的前提下,实现这个需要?

动态代理

具体做法如下:

1. 为现有的每一个类都编写一个 对应的 代理类,并且让它实现和指标类雷同的接口(假如都有)

2. 在创立代理对象时,通过结构器塞入一个指标对象,而后在代理对象的办法外部调用指标对象同名办法,并在调用前后打印日志。也就是说,代理对象 = 加强代码 + 指标对象(原对象),有了代理对象后,就不必原对象了

动态代理的缺点

程序员要手动为每一个指标类,编写对应的代理类。如果以后零碎曾经有成千盈百个类,工作量太大了。所以,当初咱们的致力方向是:如何少写或者不写代理类,却能实现代理性能?


接口创建对象的可行性剖析

温习对象的创立过程

首先,在很多初学者的印象中,类和对象的关系是这样的:

尽管晓得源代码通过 javac 命令编译后会在磁盘中失去字节码文件(.class 文件),也晓得 java 命令会启动 JVM 将字节码文件加载进内存,但也仅仅止步于此了。至于从字节码文件加载进内存到堆中产生对象,期间具体产生了什么,他们并不分明。

所谓“万物皆对象”,字节码文件也难逃“被对象”的命运。它被加载进内存后,JVM 为其创立了一个对象,当前所有该类的实例,皆以它为模板。这个对象叫 Class 对象,它是 Class 类的实例。

大家想想,Class 类是用来形容所有类的,比方 Person 类,Student 类 … 那我如何通过 Class 类创立 Person 类的 Class 对象呢?这样吗:

Class clazz = new Class();

如同不对吧,我说这是 Student 类的 Class 对象也行啊。有点晕了 …

其实,程序员是无奈本人 new 一个 Class 对象的,它仅由 JVM 创立。

  • Class 类的结构器是 private 的,杜绝了外界通过 new 创立 Class 对象的可能。当程序须要某个类时,JVM 本人会调用这个结构器,并传入 ClassLoader(类加载器),让它去加载字节码文件到内存,而后JVM 为其创立对应的 Class 对象
  • 为了不便辨别,Class 对象的表示法为:Class<String>,Class<Person>

所以借此机会,咱们无妨换种形式对待类和对象:

也就是说,要失去一个类的实例,要害是先失去该类的 Class 对象!只不过 new 这个关键字切实太不便,为咱们暗藏了底层很多细节,我在刚开始学习 Java 时甚至没意识到 Class 对象的存在。

接口 Class 和类 Class 的区别

来剖析一下接口 Class 和类 Class 的区别。以 Calculator 接口的 Class 对象和 CalculatorImpl 实现类的 Class 对象为例:

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;

public class ProxyTest {public static void main(String[] args) {
        /*Calculator 接口的 Class 对象
                  失去 Class 对象的三种形式:1.Class.forName(xxx) 
                                           2.xxx.class 
                                           3.xxx.getClass()
                  留神,这并不是咱们 new 了一个 Class 对象,而是让虚拟机加载并创立 Class 对象            
                */
        Class<Calculator> calculatorClazz = Calculator.class;
        //Calculator 接口的结构器信息
        Constructor[] calculatorClazzConstructors = calculatorClazz.getConstructors();
        //Calculator 接口的办法信息
        Method[] calculatorClazzMethods = calculatorClazz.getMethods();
        // 打印
        System.out.println("------ 接口 Class 的结构器信息 ------");
        printClassInfo(calculatorClazzConstructors);
        System.out.println("------ 接口 Class 的办法信息 ------");
        printClassInfo(calculatorClazzMethods);

        //Calculator 实现类的 Class 对象
        Class<CalculatorImpl> calculatorImplClazz = CalculatorImpl.class;
        //Calculator 实现类的结构器信息
        Constructor<?>[] calculatorImplClazzConstructors = calculatorImplClazz.getConstructors();
        //Calculator 实现类的办法信息
        Method[] calculatorImplClazzMethods = calculatorImplClazz.getMethods();
        // 打印
        System.out.println("------ 实现类 Class 的结构器信息 ------");
        printClassInfo(calculatorImplClazzConstructors);
        System.out.println("------ 实现类 Class 的办法信息 ------");
        printClassInfo(calculatorImplClazzMethods);
    }

    public static void printClassInfo(Executable[] targets){for (Executable target : targets) {
            // 结构器 / 办法名称
            String name = target.getName();
            StringBuilder sBuilder = new StringBuilder(name);
            // 拼接左括号
            sBuilder.append('(');
            Class[] clazzParams = target.getParameterTypes();
            // 拼接参数
            for(Class clazzParam : clazzParams){sBuilder.append(clazzParam.getName()).append(',');
            }
            // 删除最初一个参数的逗号
            if(clazzParams!=null && clazzParams.length != 0) {sBuilder.deleteCharAt(sBuilder.length()-1);
            }
            // 拼接右括号
            sBuilder.append(')');
            // 打印 结构器 / 办法
            System.out.println(sBuilder.toString());
        }
    }
}

运行后果:

  • 接口 Class 对象没有构造方法,所以 Calculator 接口不能间接 new 对象
  • 实现类 Class 对象有构造方法,所以 CalculatorImpl 实现类能够 new 对象
  • 接口 Class 对象有两个办法 add()、subtract()
  • 实现类 Class 对象除了 add()、subtract(),还有从 Object 继承的办法

也就是说,接口和实现类的 Class 信息除了结构器,根本类似。

既然咱们心愿通过接口创立实例,就无奈避开上面两个问题:

1. 接口办法体缺失问题

首先,接口的 Class 对象曾经失去,它形容了办法信息。

但它没办法体。

没关系,反正代理对象的办法是个空壳,只有调用指标对象的办法即可。

JVM 能够在创立代理对象时,轻易糊弄一个空的办法体,反正前期咱们会想方法把指标对象塞进去调用。

所以这个问题,勉强算是解决。

2. 接口 Class 没有结构器,无奈 new

这个问题如同无解 … 毕竟这么多年了,确实没听哪位仁兄间接 new 接口的。

然而,认真想想,接口之所以不能 new,是因为它短少结构器,它自身是具备欠缺的类构造信息的。就像一个武艺高强的大内太监(接口),他空有一身绝世神功(类构造信息),却后继无人。如果江湖上有一位妙手圣医,能克隆他的一身武艺,那么克隆人不就武艺高强的同时,还能生儿育女了吗?
所以咱们就想,JDK 有没有提供这么一个办法,比方 getXxxClass(),咱们传进一个接口 Class 对象,它帮咱们克隆一个具备雷同类构造信息,又具备结构器的新的 Class 对象呢?

至此,剖析结束,咱们无奈依据接口间接创建对象(废话)。

那动静代理是怎么创立实例的呢?它到底有没有相似 getXxxClass()这样的办法呢?


动静代理

不错,动静代理的确存在 getXxxClass()这样的办法。

咱们须要 java.lang.reflect.InvocationHandler 接口和 java.lang.reflect.Proxy 类的反对。Proxy 前面会用到 InvocationHandler,因而我打算以 Proxy 为切入点。首先,再次明确咱们的思路:

通过查看 API,咱们发现 Proxy 类有一个静态方法能够帮忙咱们。

Proxy.getProxyClass():返回代理类的 Class 对象。终于找到妙手圣医。

也就说,只有传入指标类实现的接口的 Class 对象,getProxyClass()办法即可返回代理 Class 对象,而不必理论编写代理类。这相当于什么概念?

废话不多说,开搞。

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyTest {public static void main(String[] args) {
        /*
         * 参数 1:Calculator 的类加载器(当初把 Calculator 加载进内存的类加载器)* 参数 2:代理对象须要和指标对象实现雷同接口 Calculator
         * */
        Class calculatorProxyClazz = Proxy.getProxyClass(Calculator.class.getClassLoader(), Calculator.class);
        // 以 Calculator 实现类的 Class 对象作比照,看看代理 Class 是什么类型
                System.out.println(CalculatorImpl.class.getName());
        System.out.println(calculatorProxyClazz.getName());
        // 打印代理 Class 对象的结构器
        Constructor[] constructors = calculatorProxyClazz.getConstructors();
        System.out.println("---- 结构器 ----");
        printClassInfo(constructors);
        // 打印代理 Class 对象的办法
        Method[] methods = calculatorProxyClazz.getMethods();
        System.out.println("---- 办法 ----");
        printClassInfo(methods);
    }

    public static void printClassInfo(Executable[] targets) {for (Executable target : targets) {
            // 结构器 / 办法名称
            String name = target.getName();
            StringBuilder sBuilder = new StringBuilder(name);
            // 拼接左括号
            sBuilder.append('(');
            Class[] clazzParams = target.getParameterTypes();
            // 拼接参数
            for (Class clazzParam : clazzParams) {sBuilder.append(clazzParam.getName()).append(',');
            }
            // 删除最初一个参数的逗号
            if (clazzParams != null && clazzParams.length != 0) {sBuilder.deleteCharAt(sBuilder.length() - 1);
            }
            // 拼接右括号
            sBuilder.append(')');
            // 打印 结构器 / 办法
            System.out.println(sBuilder.toString());
        }
    }
}





运行后果:

大家还记得接口 Class 的打印信息吗?

也就是说,通过给 Proxy.getProxyClass()传入类加载器和接口 Class 对象,咱们失去了一个 加强版的 Class:即蕴含接口的办法信息 add()、subtract(),又蕴含了结构器 $Proxy0(InvocationHandler),还有一些本人特有的办法以及从 Object 继承的办法。

梳理一下:

1. 原先咱们本打算间接依据接口 Class 失去代理对象,无奈接口 Class 只有办法信息,没有结构器

2. 于是,咱们想,有没有方法创立一个 Class 对象,既有接口 Class 的办法信息,同时又蕴含结构器不便创立代理实例呢?

3. 利用 Proxy 类的静态方法 getProxyClass()办法,给它传一个接口 Class 对象,它能返回一个加强版 Class 对象。也就是说 getProxyClass()的实质是:用 Class,造 Class。

要谢谢 Proxy 类和 JVM,让咱们不写代理类却间接失去代理 Class 对象,进而失去代理对象。

动态代理

动静代理:用 Class 造 Class

既然 Class<$Proxy0> 有办法信息,又有结构器,咱们试着用它失去代理实例吧:

咱们发现,newInstance()创建对象失败。因为 Class 的 newInstance() 办法底层会走无参结构器。而之前打印 $Proxy0 的 Class 信息时,咱们发现它没有无参结构,只有有参结构 $Proxy0(InvocationHandler)。那就靠它了:

constructor.newInstance()须要传入一个 InvocationHandler 对象,这里采纳匿名对象的形式,invoke()办法不做具体实现,间接返回 null

难受~


Proxy.getProxyClass()的机密

一个小问题

好不容易通过 Proxy.getProxyClass()失去代理 Class,又通过反射最终失去代理对象,当然要玩一玩:

难堪,居然产生了空指针异样。纵观整个代码,新写的 add()和 subtract()返回值是 int,不会是空指针。而再往上的代码之前编译都是通过的,应该没问题啊。再三思量,咱们发现匿名对象 InvocationHandler 的 invoke()返回 null。难道是它?做个试验:让 invoke()返回 1,而后察看后果。

后果代理对象的 add 和 subtract 都返回 1

偶合吗?应该不是。我猜:每次调用代理对象的办法都会调用 invoke(),且 invoke()的返回值就是代理办法的返回值。如果真是如此,空指针异样就能够解释了:add()和 suntract()期待的返回值类型是 int,然而之前 invoke()返回 null,类型不匹配,于是空指针异样。

以防万一,再验证一下 invoke()和代理对象办法的关系:

好了,什么都不用说了。就目前的试验来看,调用过程应该是这样:

动静代理底层调用逻辑

同样的,晓得了后果后,咱们再反推原理。

动态代理:往代理对象的结构器传入指标对象,而后代理对象调用指标对象的同名办法。

动静代理:constructor 反射创立代理对象时,须要传入 InvocationHandler,我猜,代理对象外部有一个成员变量 InvocationHandler:

果然不出所料。那么动静代理的大抵设计思路就是:

为什么这么设计?

为理解耦,也为了通用性。

如果 JVM 生成代理对象的同时生成了特定逻辑的办法体,那这个代理对象前期就没有扩大的余地,只能有一种玩法。而引入 InvocationHandler 的益处是:

  • JVM 创立代理对象时不用思考办法实现,只有造一个空壳的代理对象,难受
  • 前期代理对象想要什么样的办法实现,我写在 invocationHandler 对象的 invoke()办法里送进来便是

所以,invocationHandler 的作用,倒像是把“办法”和“办法体”拆散。JVM 只造一个空的代理对象给你,前面想怎么玩,由你本人组装。反正代理对象中有个成员变量 invocationHandler,每一个办法里只有一句话:handler.invoke()。所以调任何一个代理办法,最终都会跑去调用 invoke()办法。

invoke()办法是代理对象和指标对象的桥梁。

然而咱们真正想要的后果是:调用代理对象的办法时,去调用指标对象的办法。

所以,接下来致力的方向就是:设法在 invoke()办法失去指标对象,并调用指标对象的同名办法。

代理对象调用指标对象办法

那么,如何在 invoke()办法外部失去指标对象呢?咱们来看看能不能从 invoke()办法的形参上获取点线索:

  • Object proxy:很遗憾,是代理对象自身,而不是指标对象(不要调用,会有限递归)
  • Method method:本次被调用的代理对象的办法
  • Obeject[] args:本次被调用的代理对象的办法参数

很惋惜,proxy 不是代理对象。其实想想也晓得,创立代理对象的过程中从头至尾没有指标对象参加,所以也就无奈产生关联。而且一个接口能够同时被多个类实现,所以 JVM 也无奈判断以后代理对象想要代理哪个指标对象。但好在咱们曾经晓得本次调的办法名 (Method) 和参数(args)。咱们接下来要做的就是失去指标对象并调用同名办法,而后把参数给它。

如何失去指标对象呢?没方法,为今之计只能 new 了 … 哈哈哈哈。我靠,饶了一大圈,又是动静代理,又是 invoke()的,后果还是要手动 new?别急,先玩玩。前面会改良的:

然而这样的写法显然是倒退 30 年,一夜回到解放前。咱们须要改良一下,封装 Proxy.getProxyClass(),使得指标对象能够作为参数传入:

public class ProxyTest {public static void main(String[] args) throws Throwable {CalculatorImpl target = new CalculatorImpl();
                // 传入指标对象
                // 目标:1. 依据它实现的接口生成代理对象 2. 代理对象调用指标对象办法
        Calculator calculatorProxy = (Calculator) getProxy(target);
        calculatorProxy.add(1, 2);
        calculatorProxy.subtract(2, 1);
    }

    private static Object getProxy(final Object target) throws Exception {
        // 参数 1:轻易找个类加载器给它,参数 2:指标对象实现的接口,让代理对象实现雷同接口
        Class proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
        Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class);
        Object proxy = constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println(method.getName() + "办法开始执行...");
                Object result = method.invoke(target, args);
                System.out.println(result);
                System.out.println(method.getName() + "办法执行完结...");
                return result;
            }
        });
        return proxy;
    }
}

厉害厉害 … 惋惜,还是太麻烦了。有没有更简略的形式获取代理对象?有!

间接返回代理对象,而不是代理对象 Class

从一开始就存在,哈哈。然而我感觉 getProxyClass()切入更好了解。

public class ProxyTest {public static void main(String[] args) throws Throwable {CalculatorImpl target = new CalculatorImpl();
        Calculator calculatorProxy = (Calculator) getProxy(target);
        calculatorProxy.add(1, 2);
        calculatorProxy.subtract(2, 1);
    }

    private static Object getProxy(final Object target) throws Exception {
        Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(),/* 类加载器 */
                target.getClass().getInterfaces(),/* 让代理对象和指标对象实现雷同接口 */
                new InvocationHandler(){/* 代理对象的办法最终都会被 JVM 导向它的 invoke 办法 */
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println(method.getName() + "办法开始执行...");
                        Object result = method.invoke(target, args);
                        System.out.println(result);
                        System.out.println(method.getName() + "办法执行完结...");
                        return result;
                    }
                }
        );
        return proxy;
    }
}

编写可生成代理和可插入告诉的通用办法

下面的代码,曾经比上一篇结尾间接批改指标类好多了。再来看一下过后的四大毛病:

  1. 间接批改源程序,不合乎开闭准则。应该对扩大凋谢,对批改敞开√
  2. 如果 Calculator 有几十个、上百个办法,批改量太大√
  3. 存在反复代码(都是在外围代码前后打印日志)×
  4. 日志打印硬编码在代理类中,不利于前期保护:比方你花了一上午终于写完了,组长通知你这个性能勾销,于是你又要关上 Calculator 花十分钟删除日志打印的代码!×

应用动静代理,让咱们防止手写代理类,只有给 getProxy()办法传入 target 就能够生成对应的代理对象。然而日志打印仍是硬编码在 invoke()办法中。尽管批改时只有改一处,然而别忘了“开闭准则”。所以最好是能把日志打印独自拆出来,像指标对象一样作为参数传入。

日志打印其实就是 AOP 里的告诉概念。我打算定义一个 Advice 接口,并且写一个 MyLogger 实现该接口。

告诉接口

public interface Advice {void beforeMethod(Method method);
    void afterMethod(Method method);
}

日志打印

public class MyLogger implements Advice {public void beforeMethod(Method method) {System.out.println(method.getName() + "办法执行开始...");
    }

    public void afterMethod(Method method) {System.out.println(method.getName() + "办法执行完结...");
    }
}

测试类

public class ProxyTest {public static void main(String[] args) throws Throwable {CalculatorImpl target = new CalculatorImpl();
        Calculator calculatorProxy = (Calculator) getProxy(target, new MyLogger());
        calculatorProxy.add(1, 2);
        calculatorProxy.subtract(2, 1);
    }

    private static Object getProxy(final Object target, Advice logger) throws Exception {
        /* 代理对象的办法最终都会被 JVM 导向它的 invoke 办法 */
        Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(),/* 类加载器 */
                target.getClass().getInterfaces(),/* 让代理对象和指标对象实现雷同接口 */
                (proxy1, method, args) -> {logger.beforeMethod(method);
                    Object result = method.invoke(target, args);
                    System.out.println(result);
                    logger.afterMethod(method);
                    return result;
                }
        );
        return proxy;
    }
}

差一点完满~ 下篇讲讲更完满的做法。


类加载器补充

初学者可能对诸如“字节码文件”、Class 对象比拟生疏。所以这里花一点点篇幅介绍一下类加载器的局部原理。如果咱们要定义类加载器,须要继承 ClassLoader 类,并笼罩 findClass()办法:

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
    try {/* 本人另外写一个 getClassData()
                  通过 IO 流从指定地位读取 xxx.class 文件失去字节数组 */
        byte[] datas = getClassData(name);
        if(datas == null) {throw new ClassNotFoundException("类没有找到:" + name);
        }
        // 调用类加载器自身的 defineClass()办法,由字节码失去 Class 对象
        return this.defineClass(name, datas, 0, datas.length);
    } catch (IOException e) {e.printStackTrace();
        throw new ClassNotFoundException("类找不到:" + name);
    }
}

所以,这就是类加载之所以能把 xxx.class 文件加载进内存,并创立对应 Class 对象的深层起因。具体文章能够参考基友写的另一篇:请叫我程序猿小孩儿:好怕怕的类加载器


小结

动态代理

代理类 CalculatorProxy 是咱们当时写好的,编译后失去 Proxy.class 字节码文件。随后和指标类一起被 ClassLoader(类加载器)加载进内存,生成 Class 对象,最初生成实例对象。代理对象中有指标对象的援用,调用同名办法并前后加上日志打印。

长处:不必批改指标类源码

毛病是:高度绑定,不通用。硬编码,不易于保护。

动静代理

咱们本想通过接口 Class 间接创立代理实例,无奈的是,接口 Class 尽管有办法信息形容,却没有结构器,无奈创建对象。所以咱们心愿 JDK 能提供一套 API,咱们传入接口 Class,它主动复制外面的办法信息,造出一个有结构器、能创立实例的代理 Class 对象。

长处:

  • 不必写代理类,依据指标对象间接生成代理对象
  • 告诉能够传入,不是硬编码
    • *

彩蛋

下面的探讨都在刻意回避代理对象的类型,放最初来聊一聊。

最初讨论一下代理对象是什么类型。

首先,请辨别两个概念:代理 Class 对象和代理对象。

单从名字看,代理 Class 和 Calculator 的接口的确相去甚远,然而咱们却能讲代理对象赋值给接口类型:

但谁说是否复制给接口是看名字的?难道不是只有实现接口就行了吗?

代理对象的实质就是:和指标对象实现雷同接口的实例。代理 Class 能够叫任何名字,whatever,只有它实现某个接口,就能成为该接口类型。

我写了一个 MyProxy 类,那么它的 Class 名字必然叫 MyProxy。但这和是否赋值给接口没有任何关系。因为它实现了 Serializable 和 Collection,所以 myProxy(代理实例)同时 是这两个接口的类型。

我想了个很骚的比喻,心愿能解释分明:

接口 Class 对象是大内太监,外面的办法和字段比做他的一身武艺,然而他没有小 DD(结构器),所以不能 new 实例。一身武艺后继无人。

那怎么办呢?

失常路径(implements):

写一个类,实现该接口。这个就相当于大巷上拉了一个人,认他做干爹。一身武艺传给他,只是比他干爹多了小 DD,能够 new 实例。

非正常路径(动静代理):

通过妙手圣医 Proxy 的克隆大法(Proxy.getProxyClass()),克隆一个 Class,然而有小 DD。所以这个克隆人 Class 能够创立实例,也就是代理对象。

代理 Class 其实就是附有结构器的接口 Class,一样的类构造信息,却能创立实例。

JDK 动静代理生成的实例

CGLib 动静代理生成的实例

如果说继承的父类是亲爹(只有一个),那么实现的接口是干爹(能够有多个)。

实现接口是一个类认干爹的过程。接口无奈创建对象,但实现该接口的类能够。

比方

class Student extends Person implements A, B

这个类 new 一个实例进去,你问它:你爸爸是谁啊?它会通知你:我只有一个爸爸 Person。

然而 student instanceof A interface,或者 student instanceof B interface,它会通知你两个都是它干爹(true),都能够用来接管它。

然而,但凡无利必有弊。

也就是说,动静代理生成的代理对象,最终都能够用接口接管,和指标对象一起造成了多态,能够随便切换展现不同的性能。然而切换的同时,只能应用该接口定义的办法。

正文完
 0