动态代理调用应该转到动态类型还是静态类型的方法?

时间:2019-05-09 11:08:01

标签: java generics inheritance dynamic-proxy

动态代理接收的方法对象似乎是引用类型,而不是对象类型,但是仅当方法签名中包含泛型时。应该这样吗?

示例:

public class ProxyTest implements InvocationHandler {

    public static interface A<T> {

        void test(T t);
    }

    public static interface B extends A<String> {

        @C
        @Override
        void test(String e);
    }

    @Retention(RetentionPolicy.RUNTIME)
    public static @interface C {}

    public static void main(String[] args) {
        Class<A> a = A.class;
        Class<? extends A<String>> bAsA = B.class;
        Class<B> b = B.class;

        A aProxy = ((A) Proxy.newProxyInstance(a.getClassLoader(), new Class[] {a}, new ProxyTest()));
        A bAsAProxy = ((A) Proxy.newProxyInstance(bAsA.getClassLoader(), new Class[] {bAsA}, new ProxyTest()));
        B bProxy = ((B) Proxy.newProxyInstance(b.getClassLoader(), new Class[] {b}, new ProxyTest()));
        A bProxyAssignedToA = bProxy;

        aProxy.test("");
        bAsAProxy.test("");
        bProxy.test("");
        bProxyAssignedToA.test("");
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        System.out.println(method.getDeclaringClass().getSimpleName() + ": " + (method.getAnnotation(C.class) != null ? "C" : "null"));
        return null;
    }
}

我希望它能打印:
A:null
B:C
B:C
B:C

但实际输出是
A:null
B:空
B:C
B:空

当我将B的泛型更改为Object或将其删除时,它会正确打印:
A:null
B:C
B:C
B:C

1 个答案:

答案 0 :(得分:1)

当我使用Java 8或更高版本编译并运行您的示例时,我得到了您期望的输出:

A: null
B: C
B: C
B: C

如果将调用处理程序代码更改为

@Override
public Object invoke(Object proxy, Method method, Object[] args) {
    System.out.println(method.getDeclaringClass().getSimpleName()
        + "." + method.getName()
        + Arrays.toString(method.getParameterTypes())
        + ": " + (method.getAnnotation(C.class) != null ? "C" : "null"));
    return null;
}

您会得到

使用Java 8或更高版本编译:
A.test[class java.lang.Object]: null
B.test[class java.lang.Object]: C
B.test[class java.lang.String]: C
B.test[class java.lang.Object]: C
使用较旧的Java版本编译:
A.test[class java.lang.Object]: null
A.test[class java.lang.Object]: null
B.test[class java.lang.String]: C
A.test[class java.lang.Object]: null

为进一步说明问题,请在您的main方法中添加以下内容

Class<?>[] classes = { A.class, B.class };
for(Class<?> c: classes) {
    System.out.println(c);
    for(Method m: c.getDeclaredMethods()) {
        for(Annotation a: m.getDeclaredAnnotations())
            System.out.print(a+" ");
        System.out.println(m);
    }
    System.out.println();
}

它将打印

interface ProxyTest$A
public abstract void ProxyTest$A.test(java.lang.Object)

interface ProxyTest$B
@ProxyTest$C() public abstract void ProxyTest$B.test(java.lang.String)

使用8之前的Java版本进行编译时。

由于类型擦除,A接口仅声明类型为Object的方法,当在编译时类型为{{的引用上调用test时,该方法总是被调用。 1}}。接口A声明参数类型为B的专用版本,仅当引用的编译时类型为String时才调用该版本。

实现类必须实现通常不会注意到的两个方法,因为编译器将自动为您实现 bridge方法(此处为B),这将强制转换参数并调用实际的实现方法,此处为test(Object)。但是您确实会在生成代理时注意到,该代理将为任一方法调用您的调用处理程序,而不是实现桥逻辑。

在Java 8或更高版本下编译并运行代码时,它将打印

test(String)

现在,由于Java现在在接口中支持非interface ProxyTest$A public abstract void ProxyTest$A.test(java.lang.Object) interface ProxyTest$B @ProxyTest$C() public abstract void ProxyTest$B.test(java.lang.String) @ProxyTest$C() public default void ProxyTest$B.test(java.lang.Object) 方法,所以接口B本身就有一个桥接方法。您可能会注意到,由于参数类型,代理仍然会覆盖这两者,但是由于编译器将在实际接口方法中声明的所有注释都复制到了桥方法,因此您将在调用处理程序中看到它们。另外,声明类现在是预期的类abstract

请注意,B的运行时行为没有改变,这是编译器有所作为的。因此,您需要重新编译源代码,以从较新的版本中受益(并且结果将不会在较旧的版本上运行)。