Динамический прокси 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;
}
}
Ответы
Ответ 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
}
}
фасадИнтерфейс - это прокси-интерфейс, который объявляет метод по умолчанию, возможно, будет возможно использовать методы по умолчанию суперинтерфейса.