javagenericsinheritancedynamic-proxy

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


The method-object, which the dynamic proxy receives, seems to be of the reference type instead of the object type, but only when generics are involved in the method signature. Should it work that way?

Example:

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

I expect it to print:
A: null
B: C
B: C
B: C

but the actual output is
A: null
B: null
B: C
B: null

When I change the generic of B to Object, or remove it, it correctly prints:
A: null
B: C
B: C
B: C


Solution

  • When I compile & run your example with Java 8 or newer, I get the output you expect:

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

    If you change your invocation handler code to

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

    you’ll get

    compiled with Java 8 or newer:
    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
    
    compiled with older Java versions:
    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
    

    To illustrate the issue further, add the following to your main method

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

    and it will print

    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)
    

    when being compiled with a Java version before 8.

    Due to type erasure, the A interface only declares a method with type Object, which will always get invoked when test is invoked on a reference of compile-time type A. The interface B declares a specialized version with a String parameter type, which only gets invoked when the compile-time type of the reference is B.

    Implementation classes have to implement both methods, which you normally don’t notice, as the compiler will automatically implement a bridge method, here test(Object), for you, which will cast the argument(s) and invoke the real implementation method, here test(String). But you do notice when generating a proxy which will invoke your invocation handler for either method, instead of implementing a bridge logic.

    When you compile & run the code under Java 8 or newer, it will print

    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)
    

    Now, the interface B has a bridge method on its own, which became possible, as Java now supports non-abstract methods in interfaces. The proxy still overrides both, as you can notice due to the parameter type, but since the compiler copied all annotations declared at the actual interface method to the bridge method, you’ll see them in the invocation handler. Also, the declaring class now is the intended class B.

    Note that the runtime behavior of the Proxy did not change, it’s the compiler making a difference. So you would need to recompile your sources, to get a benefit from newer versions (and the result won’t run on older versions).