Должны ли вызовы динамического прокси переходить к методу динамического или статического типа? - PullRequest
2 голосов
/ 09 мая 2019

Объект-метод, который получает динамический прокси-сервер, кажется, имеет ссылочный тип, а не тип объекта, но только когда в сигнатуру метода включены обобщенные элементы. Должно ли это так работать?

Пример:

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: ноль
B: C
B: C
B: C

но фактический результат равен
A: ноль
B: ноль
B: C
B: ноль

Когда я меняю универсальный тип B на Object или удаляю его, он правильно печатает:
A: ноль
B: C
B: C
B: C

1 Ответ

1 голос
/ 10 мая 2019

Когда я компилирую и запускаю ваш пример с 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)

при компиляции с версией Java до 8.

Из-за стирания типа интерфейс A объявляет только метод с типом Object, который всегда вызывается при вызове test для ссылки типа компиляции A. Интерфейс B объявляет специализированную версию с параметром String, который вызывается только тогда, когда тип ссылки во время компиляции равен B.

Классы реализации должны реализовывать оба метода, которые вы обычно не замечаете, так как компилятор автоматически реализует для вас метод моста , здесь 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 теперь поддерживает не-1039 * методы в интерфейсах. Прокси все еще переопределяет оба, как вы можете заметить из-за типа параметра, но поскольку компилятор скопировал все аннотации, объявленные в фактическом методе интерфейса, в метод bridge, вы увидите их в обработчике вызовов. Кроме того, декларирующий класс теперь является предполагаемым классом B.

Обратите внимание, что поведение Proxy во время выполнения не изменилось, это - компилятор, имеющий значение. Поэтому вам нужно будет перекомпилировать исходные коды, чтобы получить выгоду от более новых версий (и результат не будет работать на более старых версиях).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...