Динамический прокси Java8 и методы по умолчанию

Имея динамический прокси для интерфейса с методами по умолчанию, как мне вызвать метод по умолчанию? Используя что-то вроде defaultmethod.invoke(this, ...), вы просто вызываете обработчик вызова прокси-сервера (что в некотором роде правильно, потому что у вас нет класса реализации для этого интерфейса).

У меня есть обходной путь, использующий ASM для создания класса, реализующего интерфейс и делегирующего такие вызовы экземпляру этого класса. Но это не очень хорошее решение, особенно если метод по умолчанию вызывает другие методы интерфейса (вы получаете делегаторный пинг-понг). JLS на удивление молчит по этому вопросу...

Вот небольшой пример кода:

public class Java8Proxy implements InvocationHandler {
    public interface WithDefaultMethod {
        void someMethod();

        default void someDefaultMethod() {
            System.out.println("default method invoked!");
        }
    }

    @Test
    public void invokeTest() {
        WithDefaultMethod proxy = (WithDefaultMethod) Proxy.newProxyInstance(
            WithDefaultMethod.class.getClassLoader(),
            new Class<?>[] { WithDefaultMethod.class }, this);
        proxy.someDefaultMethod();

    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        // assuming not knowing the interface before runtime (I wouldn't use a
        // proxy, would I?)
        // what to do here to get the line printed out?

        // This is just a loop
        // method.invoke(this, args);

        return null;
    }
}

person Cfx    schedule 05.10.2014    source источник


Ответы (5)



Принятый ответ использует setAccessible(true) для перехода к MethodHandles.Lookup, что ограничено в Java 9 и более поздних версиях. В этом почте описывается изменение JDK, которое работает для Ява 9 или более поздняя версия.

Это можно заставить работать на Java 8 (и более поздних версиях), если вы можете заставить автора интерфейса вызывать вашу утилиту с экземпляром MethodHandles.Lookup, созданным в интерфейсе (таким образом, он получает разрешение на доступ к методам по умолчанию для интерфейс):

interface HelloGenerator {
  public static HelloGenerator  createProxy() {
    // create MethodHandles.Lookup here to get access to the default methods
    return Utils.createProxy(MethodHandles.lookup(), HelloGenerator.class);
  }
  abstract String name();
  default void sayHello() {
    System.out.println("Hello " + name());
  }
}

public class Utils {
  static <P> P createProxy(MethodHandles.Lookup lookup, Class<P> type) {
    InvocationHandler handler = (proxy, method, args) -> {
        if (method.isDefault()) {
          // can use unreflectSpecial here, but only because MethodHandles.Lookup
          // instance was created in the interface and passed through
          return lookup
              .unreflectSpecial(method, method.getDeclaringClass())
              .bindTo(proxy)
              .invokeWithArguments(args);
        }
        return ...; // your desired proxy behaviour
    };

    Object proxy = Proxy.newProxyInstance(
        type.getClassLoader(), new Class<?>[] {type}, handler);
    return type.cast(proxy);
  }
}

Этот подход не справится со всеми вариантами использования Java 8, но с моим он справился.

person JodaStephen    schedule 09.04.2018
comment
Возможно, вы могли бы добавить решение из электронного письма, которое вы связали с ответом. Любой, кто читает это сегодня, вероятно, захочет использовать это решение (используя findSpecial) вместо обходного пути Java 8. - person Jorn; 11.02.2020
comment
@Jorn, и, наконец, это поддерживается нативным способом - person Eugene; 22.03.2021

Поскольку jdk-16 это поддерживается собственным способом через invokeDefault.

В вашем примере это будет сделано так:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class InvocationHandlerTest {

    public static void main(String[] args) {
        WithDefaultMethod proxy = (WithDefaultMethod) Proxy.newProxyInstance(
                WithDefaultMethod.class.getClassLoader(),
                new Class<?>[] { WithDefaultMethod.class }, new Java8Proxy());
        proxy.someDefaultMethod();
    }

     interface WithDefaultMethod {
        void someMethod();

        default void someDefaultMethod() {
            System.out.println("default method invoked!");
        }
    }

    static class Java8Proxy implements InvocationHandler {

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("invoked");
            InvocationHandler.invokeDefault(proxy, method, args);
            return null;
        }
    }

}

Но вам не нужна явная реализация того интерфейса, который вам нужен, это можно сделать немного иначе:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class InvocationHandlerTest {

    public static void main(String[] args) {

        WithDefaultMethod proxy = (WithDefaultMethod) Proxy.newProxyInstance(
                WithDefaultMethod.class.getClassLoader(),
                new Class<?>[] { WithDefaultMethod.class },
                (o, m, params) -> {
                    if (m.isDefault()) {
                        // if it's a default method, invoke it
                        return InvocationHandler.invokeDefault(o, m, params);
                    }
                    return null;
                });

        proxy.someDefaultMethod();

    }

     interface WithDefaultMethod {
        void someMethod();

        default void someDefaultMethod() {
            System.out.println("default method invoked!");
        }
    }
}
person Eugene    schedule 22.03.2021

Я написал запись в блоге с подробным описанием различных подходов, которые необходимо использовать для Java 8 и 9+: http://netomi.github.io/2020/04/17/default-methods.html

Он включает в себя код из фреймворка Spring для простой и эффективной обработки различных случаев.

person T. Neidhart    schedule 17.04.2020

Это раздражающе глупое нелогичное поведение, которое, как я утверждаю, является ошибкой в ​​методе #invoke(Object,Object[]), потому что вы не можете сделать вещи простыми в InvocationHandler, например:

if (method.isDefault())
    method.invoke(proxy, args);
else
    method.invoke(target, args); // to call a wrapped object

Поэтому нужно выполнить специальный поиск MethodHandle и привязать его к прокси-серверу, чтобы вызвать его.

Я усовершенствовал предоставленный McDowell код следующим образом (упрощенно):

private static final Constructor<MethodHandles.Lookup> lookupConstructor;

static {
    try {
        lookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
        lookupConstructor.setAccessible(true);
    } catch (NoSuchMethodException e) {
        throw new RuntimeException(e);
    }
}

private static MethodHandle findDefaultMethodHandle(Class<?> facadeInterface, Method m) {
    try {
        Class<?> declaringClass = m.getDeclaringClass();
        // Used mode -1 = TRUST, because Modifier.PRIVATE failed for me in Java 8.
        MethodHandles.Lookup lookup = lookupConstructor.newInstance(declaringClass, -1);
        try {
            return lookup.findSpecial(facadeInterface, m.getName(), MethodType.methodType(m.getReturnType(), m.getParameterTypes()), declaringClass);
        } catch (IllegalAccessException e) {
            try {
                return lookup.unreflectSpecial(m, declaringClass);
            } catch (IllegalAccessException x) {
                x.addSuppressed(e);
                throw x;
            }
        }
    } catch (RuntimeException e) {
        throw (RuntimeException) e;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

private static class InvocationHandlerImpl implements InvocationHandler {
    private final Class<?> facadeInterface;

    private Object invokeDefault(Object proxy, Method method, Object[] args) throws Throwable {
        MethodHandle mh = findDefaultMethodHandle(facadeInterface, m);
        return mh.bindTo(proxy).invokeWithArguments(args);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.isDefault()) {
            return invokeDefault(proxy, method, args);
        }
        // rest of code method calls
      }
 }

фасадИнтерфейс — это проксируемый интерфейс, который объявляет метод по умолчанию, вероятно, также можно будет использовать методы по умолчанию суперинтерфейса.

Неигрушечный код должен выполнять этот поиск перед вызовом вызова или, по крайней мере, кэшировать MethodHandle.

person Infernoz    schedule 02.01.2019