java - sirve - ¿Cómo invocar explícitamente el método predeterminado desde un Proxy dinámico?
set proxy java (4)
Dado que las interfaces de Java 8 pueden tener métodos por defecto. Sé cómo invocar el método explícitamente desde el método de implementación, es decir (consulte Llamar explícitamente a un método predeterminado en Java )
Pero, ¿cómo invoco explícitamente el método predeterminado utilizando la reflexión, por ejemplo, en un proxy?
Ejemplo:
interface ExampleMixin {
String getText();
default void printInfo(){
System.out.println(getText());
}
}
class Example {
public static void main(String... args) throws Exception {
Object target = new Object();
Map<String, BiFunction<Object, Object[], Object>> behavior = new HashMap<>();
ExampleMixin dynamic =
(ExampleMixin) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),new Class[]{ExampleMixin.class}, (Object proxy, Method method, Object[] arguments) -> {
//custom mixin behavior
if(behavior.containsKey(method.getName())) {
return behavior.get(method.getName()).apply(target, arguments);
//default mixin behavior
} else if (method.isDefault()) {
//this block throws java.lang.IllegalAccessException: no private access for invokespecial
return MethodHandles.lookup()
.in(method.getDeclaringClass())
.unreflectSpecial(method, method.getDeclaringClass())
.bindTo(target)
.invokeWithArguments();
//no mixin behavior
} else if (ExampleMixin.class == method.getDeclaringClass()) {
throw new UnsupportedOperationException(method.getName() + " is not supported");
//base class behavior
} else{
return method.invoke(target, arguments);
}
});
//define behavior for abstract method getText()
behavior.put("getText", (o, a) -> o.toString() + " myText");
System.out.println(dynamic.getClass());
System.out.println(dynamic.toString());
System.out.println(dynamic.getText());
//print info should by default implementation
dynamic.printInfo();
}
}
Edición: Sé que se ha formulado una pregunta similar en ¿Cómo invoco los métodos predeterminados de Java 8 de manera repetitiva , pero esto no ha resuelto mi problema por dos razones:
- El problema descrito en esa pregunta apuntaba a cómo invocarlo a través de la reflexión en general , por lo que no se hizo una distinción entre el método predeterminado y anulado, y esto es simple, solo se necesita una instancia.
- Una de las respuestas, mediante el uso de controles de método, solo funciona con un desagradable pirateo (imho) como cambiar los modificadores de acceso a los campos de la clase de búsqueda, que es la misma categoría de "soluciones" como esta: cambiar el campo final estático privado utilizando la reflexión de Java : es bueno saber que es posible, pero no lo usaría en la producción ; estoy buscando una forma "oficial" de hacerlo.
La IllegalAccessException
se lanza en unreflectSpecial
Caused by: java.lang.IllegalAccessException: no private access for invokespecial: interface example.ExampleMixin, from example.ExampleMixin/package
at java.lang.invoke.MemberName.makeAccessException(MemberName.java:852)
at java.lang.invoke.MethodHandles$Lookup.checkSpecialCaller(MethodHandles.java:1568)
at java.lang.invoke.MethodHandles$Lookup.unreflectSpecial(MethodHandles.java:1227)
at example.Example.lambda$main$0(Example.java:30)
at example.Example$$Lambda$1/1342443276.invoke(Unknown Source)
Si todo lo que tiene es una interfaz, y todo lo que tiene acceso es un objeto de clase es una interfaz que amplía su interfaz base, y desea llamar al método predeterminado sin una instancia real de una clase que implementa la interfaz, puede:
Object target = Proxy.newProxyInstance(classLoader,
new Class[]{exampleInterface}, (Object p, Method m, Object[] a) -> null);
Cree una instancia de la interfaz y luego construya MethodHandles.Lookup utilizando la reflexión:
Constructor<MethodHandles.Lookup> lookupConstructor =
MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, Integer.TYPE);
if (!lookupConstructor.isAccessible()) {
lookupConstructor.setAccessible(true);
}
Y luego use ese lookupConstructor
para crear una nueva instancia de su interfaz que permita el acceso privado a invokespecial
. Luego invoca el método en el target
proxy falso que hiciste anteriormente.
lookupConstructor.newInstance(exampleInterface,
MethodHandles.Lookup.PRIVATE)
.unreflectSpecial(method, declaringClass)
.bindTo(target)
.invokeWithArguments(args);
Si usa una clase implícita concreta como lookupClass y caller para invokeSpecial, debería invocar correctamente la implementación predeterminada de la interfaz (no es necesario hackear el acceso privado):
Example target = new Example();
...
Class targetClass = target.getClass();
return MethodHandles.lookup()
.in(targetClass)
.unreflectSpecial(method, targetClass)
.bindTo(target)
.invokeWithArguments();
Por supuesto, esto solo funciona si tiene una referencia a un objeto concreto que implementa la interfaz.
Edición: esta solución solo funcionará si la clase en cuestión (ejemplo en el código anterior) es accesible desde el código del llamante, por ejemplo, una clase interna anónima.
La implementación actual de la clase MethodHandles / Lookup no permitirá llamar a invokeSpecial en ninguna clase que no sea de acceso privado desde la clase de llamante actual. Hay varias soluciones disponibles, pero todas requieren el uso de la reflexión para hacer accesibles los constructores / métodos, que probablemente fallarán en caso de que se instale un SecurityManager.
También he tenido problemas con problemas similares al usar MethodHandle.Lookup
en JDK 8 - 10, que se comportan de manera diferente. He blogeado sobre la solución correcta aquí en detalle .
Este enfoque funciona en Java 8.
En Java 8, el enfoque ideal utiliza un hack que accede a un constructor privado de paquetes desde Lookup
:
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Constructor;
import java.lang.reflect.Proxy;
interface Duck {
default void quack() {
System.out.println("Quack");
}
}
public class ProxyDemo {
public static void main(String[] a) {
Duck duck = (Duck) Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class[] { Duck.class },
(proxy, method, args) -> {
Constructor<Lookup> constructor = Lookup.class
.getDeclaredConstructor(Class.class);
constructor.setAccessible(true);
constructor.newInstance(Duck.class)
.in(Duck.class)
.unreflectSpecial(method, Duck.class)
.bindTo(proxy)
.invokeWithArguments();
return null;
}
);
duck.quack();
}
}
Este es el único enfoque que funciona con interfaces tanto de acceso privado como de acceso privado. Sin embargo, el enfoque anterior hace que el acceso reflexivo ilegal a las partes internas de JDK, que ya no funcionará en una versión futura de JDK, o si se especifica --illegal-access=deny
en la JVM.
Este enfoque funciona en Java 9 y 10, pero no 8
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Proxy;
interface Duck {
default void quack() {
System.out.println("Quack");
}
}
public class ProxyDemo {
public static void main(String[] a) {
Duck duck = (Duck) Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class[] { Duck.class },
(proxy, method, args) -> {
MethodHandles.lookup()
.findSpecial(
Duck.class,
"quack",
MethodType.methodType(void.class, new Class[0]),
Duck.class)
.bindTo(proxy)
.invokeWithArguments();
return null;
}
);
duck.quack();
}
}
Solución
Simplemente implemente las dos soluciones anteriores y verifique si su código se está ejecutando en JDK 8 o en un JDK posterior y estará bien. Hasta que no estés :)
Utilizar:
Object result = MethodHandles.lookup()
.in(method.getDeclaringClass())
.unreflectSpecial(method, method.getDeclaringClass())
.bindTo(target)
.invokeWithArguments();