Подтвердить что ты не робот

Динамический прокси 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;
    }
}
4b9b3361

Ответ 1

Вы можете использовать MethodHandles тип вашего InvocationHandler. Этот код копируется из Zero Turnaround.

Constructor<MethodHandles.Lookup> constructor;
Class<?> declaringClass;
Object result;

if (method.isDefault()) {
   declaringClass = method.getDeclaringClass();
   constructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);

   constructor.setAccessible(true);

   result = constructor.
      newInstance(declaringClass, MethodHandles.Lookup.PRIVATE).
      unreflectSpecial(method, declaringClass).
      bindTo(proxy).
      invokeWithArguments(args);

   return(result);
}

Ответ 2

Принятый ответ использует setAccessible(true), чтобы взломать MethodHandles.Lookup, что ограничено в Java 9 и выше. Это письмо описывает изменение JDK, которое работает для Java 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, но он справился с моим.

Ответ 3

A Proxy реализует все методы поддерживаемых интерфейсов. Все они ссылаются на InvocationHandler, созданный с помощью Proxy.

A Proxy предназначен для делегирования вызова фактическому экземпляру. Метод default уже переопределен прокси-сервером и поэтому не может быть вызван напрямую. Proxy перехватит все вызовы и передаст их в InvocationHandler.

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

Ответ 4

Я нашел эту статью чрезвычайно полезной при попытке понять проблему рефлексивного вызова метода интерфейса по умолчанию на прокси-интерфейсе этого интерфейса.

Ответ 5

Я уточнил код, предоставленный 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
      }
 }

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