动态代理类(翻译)

原文: Dyanmic Proxy Classes

介绍

一个动态代理类是实现了多个接口存在于运行时的类,这样,一个动态代理类所实现的接口的方法的调用会被编译并且通过一个统一的接口分配给另一个对象。因此,一个动态代理类可以在不预先生成代理类的情况下创建一个类型安全又实现了多个接口的代理对象,就像使用compile-time工具。动态代理类对象的方法调用被分配给InvocationHandler的对象的一个单独的方法,然后通过java.lang.reflect.Method的对象根据参数确定方法编译和执行。

应用或者是类库需要类型安全的反射传递、对象调用来实现接口API时,使用动态代理会很有帮助。例如,一个应用可以利用动态代理创建一个实现了多个任意事件监听接口的对象,而这些接口都是扩展了java.util.EventListener接口,这样这个对象就可以用一种统一的风格来处理不同类型的事件,比如把所有这种事件记录到日志文件中。

动态代理类API

动态代理类(以下用代理类表示)是实现了多个接口并在运行时创建的类。

代理接口指被代理类实现的接口。

代理对象指代理类的实例对象。

创建代理类

代理类,代理对象用java.lang.reflect.Proxy的静态方法创建。

Proxy.getProxyClass方法用代理类的类加载器和一个接口数组返回一个java.lang.Class对象。代理类将根据指定的类加载器定义并且实现所有指定的接口。如果类加载器已经存在了定义过的实现了相同排列接口的代理类,则返回该代理类。其它情况下,实现了这些接口的代理类会动态的生成并在指定的加载器中定义。

传递给Proxy.getProxyClass的参数有如下一些限制:

  • 接口数组中的所有Class对象必须表示的是接口,不是类或原始类型。
  • 接口数组中不能有两个元素指向相同的Class对象。
  • 所有的接口类型对于指定的类加载器必须是可视的,换句话说,对于类加载器cl和每一个接口iClass.forName(i.getName(),false,cl)==i必须为true。
  • 所有非共有的接口必须在相同的包下,否则代理类实现所有的接口则不可能,不管定义的是哪个包。
  • 对任何指定的接口的成员方法集拥有相同的特征:
    • 如果有一个方法返回的类型是原始类型或void,则所有的方法必须指定相同的类型。
    • 另外,任何一个方法的返回类型必须是可以指定给其它方法的返回类型。
  • 生成的代理类必须不超过虚拟机对类的任何限制。比如,虚拟机可以限制一个类的实现接口数为65535,则接口的数组大小必须不超过65535。

违法上面的任何一项限制,Proxy.getProxyClass将抛出IllegalArgumentException。如果接口数组参数或者其中的元素有null,则会抛出NullPointerException

需要注意指定的代理接口的顺序是有意义的: 两个用相同的接口组合但顺序不同的代理类的请求会生成两个不同的代理类。代理类因为代理接口的顺序不同而有区别是为了提供确定的方法调用编码,防止两个或更多的接口共用同一个有相同名字和参数标识的方法。详细的描述可以参阅多个代理接口的重复方法

所以通过Proxy.getProxyClass传递相同的类加载器和接口并不谁生成一个新的代理类,动态代理类的实现应该存在一个代理类的缓存中,关键字就是与其一致的类加载器和接口序列。代理类的实现除了应该注意类加载器、接口外,还要注意这种方式的的代理类在占用时同时会阻止类的加载器及其含有的所有类被垃圾回收。

代理类的性质

一个代理类含有如下的性质:

  • 代理类是public,final且非abstract.
  • 代理类的绝对名字没有详细指定,但为代理类的名字保留以字符串"$Proxy"开头。
  • 代理类继承了java.lang.reflect.Proxy
  • 代理类在创建的过程中完整有序实现了指定的接口。
  • 如果一个代理类实现了一个non-public的接口,则代理类会生成在该接口所在的包下。其它情况,代理类的包未被指定。注意在运行时闭包行为不会阻止一个代理类在特定包下的成功定义,已经在相同的有特定签名的包下和相同的类加载器中定义的类也不会。
  • 因为代理类在创建时实现了所有指定的接口,所以代理类的Class对象可以调用getInterfaces得到一个包含相同序列的接口数组(按代理类创建时指定的序列),调用getMethods得到包含所有实现接口的所有方法对象的数组,调用getMethod得到想要的代理接口中的方法对象。
  • Proxy.isProxyClass执行时当传递的代理类是通过Proxy.getProxyClass返回的一个类或通过Proxy.newProxyInstance返回的指定代理类的对象则为true,其它情况为false。这个方法的可靠性对制定安全决策是重要的,所以它的实现不仅仅可以测试一个继承java.lang.reflect.Proxy的类的问题。
  • 代理类的java.security.ProtectionDomain和被引导类载入器装在的系统类的保护域相同,比如java.lang.Object,因为代理类的代码是被信任的系统代码生成的。这个保护域会典型的被授予java.security.AllPermission

创建代理对象

每个代理类有一个public,参数为一个实现了InvocationHandler接口的对象的构造方法。

每个代理对象含有一个关联的通过构造方法传进来的InvocationHandler对象。代理对象的创建除了用反射的API进入public构造方法实现外,还可以调用Proxy.newProxyInstance方法来实现,这个方法综合了Proxy.getProxyClass并调用了其含有InvocationHandler参数的构造方法。Proxy.newProxyInstance如果抛出IllegalArgumentException,原因和Proxy.getProxyClass的相同。

代理对象的性质

代理对象有如下的性质:

  • 创建一个代理对象proxy,它的代理类实现的一个接口Foo,proxy instanceof Foo会返回true; (Foo) proxy类型转换会成功(不会抛出ClassCastException)。

  • 静态方法Proxy.getInvocationHandler会返回代理对象在创建时关联的InvocationHandler对象。如果传给Proxy.getInvocationHandler的参数不是一个代理对象,则会抛出IllegalArgumentException

  • 代理对象调用的接口方法会被编译并分配给InvocationHandler对象的invoke方法,详细描述:

    代理对象自身会作为invoke方法的第一个参数传值,类型是Object。

    invoke方法的第二个参数传的是与代理对象调用的接口方法一致的java.lang.reflect.Method对象。声明的方法类是接口中已经定义过的产生的对象,方法类也可以是代理类通过继承父代理接口里的。

    invoke的第三个参数是传入的代理对象的方法调用的对象数组。原始类型应该用包装类的Class对象,例如java.lang.Integerjava.lang.Booleaninvoke方法的实现可以灵活的修改数组的内容。

    invoke方法的返回代理对象调用的方法的返回值。如果接口中方法声明的返回类型是原始类型,则invoke返回的值必须是初始类型的包装类的对象;其它情况,则必须是已经声明过的返回类型。如果ivoke返回的null且接口方法返回的类型是原始类型,则代理对象调用该方法会抛出NullPointerException。如果invoke方法返回的类型超过声明的类型,则代理对象会抛出ClassCastException

    如果invoke方法抛出一个异常,则代理对象调用对应的方法时也会抛出异常。抛出的异常类型必须是接口方法已经声明的或是非检查异常类型java.lang.RuntimeException或java.lang.Error。如果invoke方法抛出的异常是一个检查异常并且没有在接口方法声明,则代理对象调用方法时会抛出UndeclaredThrowableExceptionUndeclaredThrowableException会被invoke方法抛出的异常重构。

  • 代理对象中调用hashCode,equals或者是toString等在java.lang.Object中定义的方法会被编码并分配给InvocationHandlerinvoke方法,和上面描述的接口方法的调用被编码和分配的方式一样。声明的方法类对象传给invoke时是java.lang.Object。代理对象从java.lang.Object继承的其它放不会被代理类重写,所以调用这些方法就像java.lang.Object的对象调用。

多个代理接口的重复方法

当一个代理类实现的两个或更多的接口中包含相同名字和参数标签的方法时,代理类的实现的接口的顺序就特别的重要。当重复的方法被代理对象调用时,方法类对象传递给InvocationHandler不一定是代理对象调用的接口的含有相同标签的方法。这个限制的存在是因为代理类的生成的相同的方法不能决定那个方法会被调用。因此,当代理对象调用重复的方法时,代理中包含该方法的最靠前的接口(不管是直接定义的还是从父接口中继承的)的方法对象会被传递给InvocationHandler对象的invoke方法,忽略已经传进来的方法调用的参数类型。

如果代理对象包含一个方法与hashCode, equals或者toStringjava.lang.Object中的方法拥有相同的名字和参数标志,那代理对象调用该方法时,传递给InvocationHandler对象的invoke方法是声明该方法的类中的java.lang.Object方法。换句话说,java.lang.Objectpublic,non-final方法逻辑上高于传给InvocationHandler的代理接口的方法。

注意当一个重复的方法传递给InvocationHandler时,invoke方法可能只会抛出可以调用的代理接口的方法抛出的异常。如果invoke方法抛出一个没有在可以调用的代理的接口的方法中指定的检查异常,则代理对象调用时会抛出UndeclaredThrowableException。这个限制表明通过getExceptionTypes返回的所有的传递给invoke方法的方法对象的异常并不都会被成功的抛出。

序列化

由于java.lang.reflect.Proxy实现了java.io.Serializable接口,所以代理对象是可以序列化的。然而,如果代理对象包含的InvocationHandler没有实现java.io.Serializable,则代理对象被写入java.io.ObjectOutputStream是会抛出java.io.NotSerializableException。对于代理类,实现java.io.Externalizable与实现java.io.Serializable的效果相同:Externalizable接口的方法writeExternalreadExternal永远不会再代理对象的序列化过程中被调用。就像所有的Class对象,代理类的Class对象也是可以序列化的。

一个代理类没有序列化的变量和一个serialVersionUID。即,代理类的Class对象传递给java.io.objectStreamClass的静态方法lookup,返回的ObjectStreamClass对象有下面的性质:

  • 调用它的getSerialVersionUID将方法返回OL。
  • 调用它的getFields方法会返回一个长度为0的数组。
  • 调用它的getField``方法参数为任意的String将返回null

支持对象序列化的流协议支持一个名字为TC_PROXYCLASSDESC的类型代码,它是流格式语法的一个终止符号。java.io.ObjectStreamConstants接口中定义了它的类型和值:

`final static byte TC_PROXYCLASSDESC = (byte)0x7D;`

语法还包括连个规则,第一个是替代扩展原始newClassDesc规则:

newClassDesc:

TC_PROXYCLASSDESC newHandle proxyClassDescInfo

proxyClassDescInfo:

(int)<count> proxyInterfaceName[count] classAnnotation superClassDesc

proxyInterfaceName: (utf)

ObjectOutputStream序列化一个类,可以用Proxy.isProxyClass判断Class对象是否是代理类。如果是代理类,则用TC_PROXYCLASSDESC类型码代替TC_CLASSDESC。proxyClassDescInfo的扩展中,proxyInterfaceName的队列是代理类实现的所有接口的名字,顺序是Class对象调用getInterfaces返回的顺序。classAnnotationsuperClassDesc执行classDescInfo规则时意义相同。对于代理类,superClassDesc是父类的描述符。java.lang.reflect.Proxy,包括序列化的代理类和它演变的代理对象。

对于不是代理类的类,ObjectOutputStream调用它的protectedannotateClass方法允许它的子类为一个特定类的流写入自定义数据。对于代理类,则会用java.io.ObjectOutputStream下面的方法代替annotateClass:

protected void annotateProxyClass(Class cl) throws IOException;

实现ObjectOutputStream的默认annotateProxyClass方法不做任何操作。

ObjectInputStream遇到类型符TC_PROXYCLASSDESC,表示反序列化流中的代理类。对于代理类,java.io.ObjectInputStream根据类的描述符区分,并用下面的方法替代resolveClass来解析Class对象:

protected Class resolveProxyClass(String[] interfaces)
    throws IOException, ClassNotFoundException;

要反序列化的代理类实现的接口名字的数组作为参数传递给resolveProxyClass方法。

ObjectInputStream的实现的默认resolveProxyClass方法返回值是调用Proxy.getProxyClass``的Class对象的interfaces参数的接口名字列表。每个用到的Class对象的接口名字i调用下面方法得到:

Class.forName(i, false, loader)

loader是执行栈中第一个非空的类加载器,如果执行栈中没有非空的类加载器则传null。这是resolveClass方法默认的选择类加载器的行为。和传递的Proxy.getProxyClass的类加载器的值相同。如果Proxy.getProxyClass抛出IllegalArgumentException,则resolveClass会抛出一个包含IllegalArgumentExceptionClassNotFoundException异常。因为代理类没有自己的序列化属性,代理对象的流的classdata[]包含了完整的对象数据,包含它的父类java.lang.reflect.Proxy。Proxy有一个序列化的属性,h,包含了代理对象的InvocationHandler。

示例

这是一个在一个实现了任意接口的对象的方法的执行前后打印一条信息的简单例子:

public interface Foo {
Object bar(Object obj) throws BazException;
}

public class FooImpl implements Foo {
Object bar(Object obj) throws BazException {
    // ...
    }
}

public class DebugProxy implements java.lang.reflect.InvocationHandler {

    private Object obj;

    public static Object newInstance(Object obj) {
        return java.lang.reflect.Proxy.newProxyInstance(
            obj.getClass().getClassLoader(),
            obj.getClass().getInterfaces(),
            new DebugProxy(obj));
    }

    private DebugProxy(Object obj) {
        this.obj = obj;
    }

    public Object invoke(Object proxy, Method m, Object[] args)
        throws Throwable
    {
        Object result;
        try {
            System.out.println("before method " + m.getName());
            result = m.invoke(obj, args);
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        } catch (Exception e) {
            throw new RuntimeException("unexpected invocation exception: " +
                                   e.getMessage());
        } finally {
            System.out.println("after method " + m.getName());
        }
        return result;
    }
}

构造一个实现了接口FooDebugProxy,并调用它的方法

Foo foo = (Foo) DebugProxy.newInstance(new FooImpl());
foo.bar(null);

这是一个实用的InvocationHandler的例子,提供了继承于java.lang.Object的默认的代理方法行为,实现了特定代理的委托的方法的调用以区别与那些依靠接口调用方法的对象:

import java.lang.reflect.*;

public class Delegator implements InvocationHandler {

// preloaded Method objects for the methods in java.lang.Object
private static Method hashCodeMethod;
private static Method equalsMethod;
private static Method toStringMethod;
static {
    try {
        hashCodeMethod = Object.class.getMethod("hashCode", null);
        equalsMethod =
            Object.class.getMethod("equals", new Class[] { Object.class });
        toStringMethod = Object.class.getMethod("toString", null);
    } catch (NoSuchMethodException e) {
        throw new NoSuchMethodError(e.getMessage());
    }
}

private Class[] interfaces;
private Object[] delegates;

public Delegator(Class[] interfaces, Object[] delegates) {
    this.interfaces = (Class[]) interfaces.clone();
    this.delegates = (Object[]) delegates.clone();
}

public Object invoke(Object proxy, Method m, Object[] args)
    throws Throwable
{
    Class declaringClass = m.getDeclaringClass();

    if (declaringClass == Object.class) {
        if (m.equals(hashCodeMethod)) {
            return proxyHashCode(proxy);
        } else if (m.equals(equalsMethod)) {
            return proxyEquals(proxy, args[0]);
        } else if (m.equals(toStringMethod)) {
            return proxyToString(proxy);
        } else {
            throw new InternalError(
                "unexpected Object method dispatched: " + m);
        }
    } else {
        for (int i = 0; i < interfaces.length; i++) {
            if (declaringClass.isAssignableFrom(interfaces[i])) {
                try {
                    return m.invoke(delegates[i], args);
                } catch (InvocationTargetException e) {
                    throw e.getTargetException();
                }
            }
        }

        return invokeNotDelegated(proxy, m, args);
    }
}

protected Object invokeNotDelegated(Object proxy, Method m,
                                    Object[] args)
    throws Throwable
{
    throw new InternalError("unexpected method dispatched: " + m);
}

protected Integer proxyHashCode(Object proxy) {
    return new Integer(System.identityHashCode(proxy));
}

protected Boolean proxyEquals(Object proxy, Object other) {
    return (proxy == other ? Boolean.TRUE : Boolean.FALSE);
}

protected String proxyToString(Object proxy) {
    return proxy.getClass().getName() + '@' +
        Integer.toHexString(proxy.hashCode());
}
}

Delegator的父类可以重写invokeNotDelegated实现调用的代理方法的行为而不用直接委托给其它对象。也可以重写proxyHashCode,proxyEqualsproxyToString,重写代理类从java.lang.Object继承的方法的默认行为。

构造一个实现了Foo接口的Delegator

Class[] proxyInterfaces = new Class[] { Foo.class };
Foo foo = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
    proxyInterfaces,
    new Delegator(proxyInterfaces, new Object[] { new FooImpl() }));

注意实现的Delegator类更多是一个说明并不完善;例如,为hashCodeequalstoString替换缓存和比较的Method对象,它只是能根据它们的名字匹配,因为java.lang.Object中没用重载任何这些方法。

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

推荐阅读更多精彩内容