详解Java动态代理机制

     之前介绍的反射和注解都是Java中的动态特性,还有即将介绍的动态代理也是Java中的一个动态特性。这些动态特性使得我们的程序很灵活。动态代理是面向AOP编程的基础。通过动态代理,我们可以在运行时动态创建一个类,实现某些接口中的方法,目前为止该特性已被广泛应用于各种框架和类库中,例如:Spring,Hibernate,MyBatis等。理解动态代理是理解框架底层的基础。
     主要内容如下:

  • 理解代理是何意
  • Java SDK实现动态代理
  • 第三方库cglib实现动态代理

一、代理的概念
     单从字面上理解,代理就是指原对象的委托人,它不是原对象但是却有原对象的权限。Java中的代理意思类似,就是指通过代理来操作原对象的方法和属性,而原对象不直接出现。这样做有几点好处:

  • 节省创建原对象的高开销,创建一个代理并不会立马创建一个实际原对象,而是保存一个原对象的地址,按需加载
  • 执行权限检查,保护原对象
这里写图片描述

实际上代理堵在了原对象的前面,在代理的内部往往还是调用了原对象的方法,只是它还做了其他的一些操作。下面看第一种实现动态代理的方式。

二、Java SDK实现动态代理
     实现动态代理主要有如下几个步骤:

  • 实现 InvocationHandler接口,完成自定义调用处理器
  • 通过Proxy的getProxyClass方法获取对应的代理类
  • 利用反射技术获取该代理类的constructor构造器
  • 利用constructor构造代理实例对象

在一步步解析源码之前,我们先通过一个完整的实例了解下,整个程序的一步步逻辑走向。

//定义了一个调用处理器
public class MyInvotion implements InvocationHandler {

    private Object realObj;

    public MyInvotion(Object obj){
        this.realObj =  obj;
    }
    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
        //通过代理执行原对象的方法
        return method.invoke(realObj,args);
    }
}
//定义一个接口
public interface MyInterface {
    
    public void sayHello();
}

//该接口的一个实现类,该类就是我们的原对象
public class ClassA implements MyInterface {

    public void sayHello(){
        System.out.println("hello walker");
    }
}
//main 函数
public static void main(String[] args){
        ClassA a = new ClassA();
        MyInvotion myInvotion = new MyInvotion(a);
        Class myProxy = Proxy.getProxyClass(ClassA.class.getClassLoader(), new Class[]{MyInterface.class});
        Constructor constructor =myProxy.getConstructor(new Class[]{InvocationHandler.class});
        MyInterface m = (MyInterface)constructor.newInstance(myInvotion);
        m.sayHello();
    }
输出结果:hello walker

简单说下整体的运行过程,首先我们创建ClassA 实例并将它传入自定义的调用处理器MyInvotion,在MyInvotion中用realObj接受该参数代表原对象。接着调用Proxy的getProxyClass方法,将ClassA 的类加载器和ClassA 的实现的接口集合传入,该方法内部会实现所有接口返回该类的代理类,然后我们利用反射获取代理类的构造器并创建实例。

以上便是整个程序运行的大致流程,接下来我们从源代码的角度看看具体是如何实现的。首先我们看InvocationHandler接口,这是我们的调用处理器,在代理类中访问的所有的方法都会被转发到这执行,具体的等我们看了代理类源码及理解了。该接口中唯一的方法是:

public Object invoke(Object proxy, Method method, Object[] args)
  • 参数Proxy表示动态生成的代理类的对象,基本没啥用
  • 参数method表示当前正在被调用的方法
  • 数组args指定了该方法的参数集合

我们上例中对该接口的实现情况,定义了一个realObj用于保存原对象的引用。重写的invoke方法中调用了原对象realObj的method方法,具体谁来调用该方法以及传入的参数是什么,在看完代理类源码即可知晓。

接下来我们看看最核心的内容,如何动态创建代理类。这是getProxyClass方法的源码:

    public static Class<?> getProxyClass(ClassLoader loader,
                                         Class<?>... interfaces)
        throws IllegalArgumentException
    {
        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        return getProxyClass0(loader, intfs);
    }

首先获取了该类实现的所有的接口的集合,然后判断创建该代理是否具有安全性问题,检查接口类对象是否对类装载器可见等。然后调用另外一个getProxyClass0方法,我们跟进去:

    private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }

        // If the proxy class defined by the given loader implementing
        // the given interfaces exists, this will simply return the cached copy;
        // otherwise, it will create the proxy class via the ProxyClassFactory
        return proxyClassCache.get(loader, interfaces);
    }

判断如果该类的接口超过65535(想必还没有那么牛的类),抛出异常。在我们的Proxy类中有个属性proxyClassCache,这是一个WeakCache类型的静态变量。它指示了我们的类加载器和代理类之间的映射。所以proxyClassCache的get方法用于根据类加载器来获取Proxy类,如果已经存在则直接从cache中返回,如果没有则创建一个映射并更新cache表。具体创建一个Proxy类并存入cache表中的代码限于能力,未能参透。

至此我们就获取到了该ClassA类对应的代理类型,接着我们通过该类的getConstructor方法获取该代理类的构造器,并传入InvocationHandler.class作为参数,至于为何要传入该类型作为参数,等会看代理类源码变一目了然了。

最后newInstance创建该代理类的实例,实现对ClassA对象的代理。

可能看完上述的介绍,你还会有点晕。下面我们通过查看动态生成的代理类的源码来加深理解。上述getProxyClass方法会动态创建一个代理类并返回他的Class类型,这个代理类一般被命名为$ProxyN,这个N是递增的用于标记不同的代理类。我们可以利用反编译工具反编译该class:

final class $Proxy0 extends Proxy implements MyInterface {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public $Proxy0(InvocationHandler paramInvocationHandler) {
        super(paramInvocationHandler);
    }

    public final boolean equals(Object paramObject) {
        return ((Boolean) this.h.invoke(this, m1, 
                new Object[] { paramObject })).booleanValue();
    }

    public final void sayHello() {
        this.h.invoke(this, m3, null);
    }

    public final String toString() {
        return (String) this.h.invoke(this, m2, null);
    }

    public final int hashCode() {
        return ((Integer) this.h.invoke(this, m0, null)).intValue();
    }

    static {
        m1 = Class.forName("java.lang.Object").getMethod("equals",
                new Class[] { Class.forName("java.lang.Object") });
        m3 = Class.forName("laoma.demo.proxy.SimpleJDKDynamicProxyDemo$IService")
                .getMethod("sayHello",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]);
    }
}

这就是上述的ClassA的动态代理类,我们看到该类的构造方法传入参数InvocationHandler类型,并调用了父类Proxy的构造方法保存了这个InvocationHandler实例,这也解释了我们为什么在获取构造器的时候需要指定参数类型为InvocationHandler,就是因为动态代理类只有一个构造器并且参数类型为InvocationHandler。

接着我们看其中的方法,貌似只有一个sayHello是我们知道的,别的方法哪来的?我们说过在动态创建代理类的时候,会实现原对象的所有接口。所以sayHello方法是实现的MyInterface。而其余的四个方法是代理类由于比较常用,被默认添加到其中。而这些方法的内部都是调用的this.h.invoke这个方法,this.h就是保存在父类Proxy中的InvocationHandler实例(我们用构造器向其中保存的),调用了这个类的invoke方法,在我们自定义的InvocationHandler实例中重写了invoke方法,我们写的比较简单,直接执行传入的method。

也就是我们调用代理类的任何一个方法都会转发到该InvocationHandler实例中的involve中,因为该实例中保存有我们的原对象,所以我们可以选择直接调取原对象中的方法作为回调。

以上便是有关Java SDK中动态代理的相关内容,稍微总结下,首先我们通过实现InvocationHandler自定义一个调用处理类,该类中会保存我们的原对象,并提供一个invoke方法供代理类使用。然后我们通过getProxyClass方法动态创建代理类,最后用反射获取代理类的实例对象。

需要注意的是:以上我们使用的四步创建代理实例时最根本的,其实Proxy中提供一个方法可以封装2到4步的操作。上述代码也可以这么写:

ClassA a = new ClassA();
MyInterface aProxy = (MyInterface)Proxy.newProxyInstance(ClassA.class.getClassLoader(),new Class<?>[]{MyInterface.class},new MyInvotion(a));
aProxy.sayHello();

我们打开该方法的内部源码,其实走的还是我们上述的过程,它就是做了封装。

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
{

    Objects.requireNonNull(h);
        
        //获取所有接口
        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }
        
        //创建动态代理类
        Class<?> cl = getProxyClass0(loader, intfs);

        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});
    .............
    ..............
}

二、第三方库cglib实现动态代理
     使用动态代理,我们编写通用的代码逻辑,即仅实现一个InvocationHandler实例完成对多个类型的代理。但是我们从动态生成的代理类的源码可以看到,所有的代理类都继承自Proxy这个类,这就导致我们这种方式不能代理类,只能代理接口。因为java中是单继承的。也就是说,给我们一个类型,我们只能动态实现该类所有的接口类型,但是该类继承的别的类我们在代理类中是不能使用的,因为它没有被代理类继承。下面看个例子:

public class ClassB {
    public void welcome(){
        System.out.println("welcom walker");
    }
}
public interface MyInterface {

    public void sayHello();
}

//需要被代理的原类型,继承了ClassB和接口MyInterface 
public class ClassA extends ClassB implements MyInterface {

    public void sayHello(){
        System.out.println("hello walker");
    }
}
//InvocationHandler 实例
public class MyInvotion implements InvocationHandler {

private Object realObj;

public MyInvotion(Object obj){
     this.realObj =  obj;
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
        return method.invoke(realObj,args);
    }
}

我们反编译该代理类,和上述的源码是一样的,此处不再重复贴出。我们能从中看出来的是,我们的代理只会实现原类型中所有的接口,至于原类型所继承的类,在生成Proxy代理类的时候会丢弃,因为所有的代理类必须继承Proxy类,这就导致原类型的父类中的方法 在代理类中丢失。这是该种方式的一大弊端。下面我们看看另一种方式实现动态代理,该种方式完美解决了这种不足。

限于篇幅,我们下篇介绍cglib实现动态代理机制的内容,本篇暂时结束,总结的不好,望海涵。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容