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