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

Should calls on a dynamic proxy go to the method of the dynamic type or the static type?

动态代理接收的方法对象似乎是引用类型而不是对象类型,但只有在方法签名中涉及泛型时才会出现。它应该那样工作吗?

示例:

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

我希望它打印:
答:空
B: C
B: C
乙:乙

但实际输出是
答:空
B: 空
B: C
B: 空

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

当我使用 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 在 [=55= 的引用上调用时,它总是会被调用] 输入 A。接口 B 声明了一个具有 String 参数类型的专用版本,仅当引用的 compile-time 类型为 B.

时才会调用该版本

实现 classes 必须实现这两个方法,你通常不会注意到,因为编译器会自动实现一个 bridge 方法,这里 test(Object),对你来说,它将转换参数并调用真正的实现方法,这里 test(String)。但是您在生成代理时确实注意到了,该代理将为任一方法调用您的调用处理程序,而不是实现桥接逻辑。

当您编译 & 运行 Java 8 或更高版本的代码时,它将打印

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 有一个自己的桥接方法,这成为可能,因为 Java 现在支持接口中的非 abstract 方法。由于参数类型,代理仍然覆盖两者,正如您可以注意到的那样,但是由于编译器将在实际接口方法中声明的所有注释复制到桥接方法,您将在调用处理程序中看到它们。此外,声明 class 现在是预期的 class B.

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