¿Cómo invocar explícitamente el método predeterminado desde un proxy dinámico?


Desde Java 8 las interfaces podrían tener métodos predeterminados. Sé cómo invocar el método explícitamente de la aplicación del método, es decir, (ver Llamar explícitamente a un método predeterminado en Java )

Pero, ¿cómo puedo invocar explícitamente el método predeterminado usando 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();
  }
}

Edit: Sé que se ha hecho una pregunta similar en Cómo invoco los métodos predeterminados de Java 8 refletively, pero esto tiene no resolví mi problema por dos razones:

  • el problema descrito en esa pregunta apuntaba a cómo invocarlo a través de reflexión en general - así que no se hizo distinción entre el método predeterminado y el anulado - y esto es simple, solo necesita una instancia.
  • una de las respuestas-usando manijas de métodos - solo funciona con nasty hack (imho) como cambiar modificadores de acceso a campos de la clase lookup, que es la misma categoría de "soluciones" como esta: Cambiar campo final estático privado usando reflexión Java: es bueno saber que es posible, pero no lo usaría en producción - Estoy buscando una forma "oficial" de hacerlo.

El 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)
Author: Community, 2016-06-14

4 answers

Si utiliza una clase impl concreta como clase de búsqueda y llamada para el invokeSpecial, debe invocar correctamente la implementación predeterminada de la interfaz (no se necesita hackear el acceso privado):

Example target = new Example();
...

Class targetClass = target.getClass();
return MethodHandles.lookup()
                    .in(targetClass)
                    .unreflectSpecial(method, targetClass)
                    .bindTo(target)
                    .invokeWithArguments();

Esto por supuesto solo funciona si tiene una referencia a un objeto concreto que implementa la interfaz.

Editar: esta solución solo funcionará si la clase en cuestión (Ejemplo en el código anterior), es privada accesible desde el código de llamada, por ejemplo, un interno anónimo clase.

La implementación actual de la clase MethodHandles/Lookup no permitirá llamar a invokeSpecial en ninguna clase que no sea privada accesible desde la clase llamante actual. Hay varias soluciones disponibles, pero todas ellas requieren el uso de la reflexión para hacer que los constructores/métodos sean accesibles, lo que probablemente fallará en caso de que se instale un SecurityManager.

 6
Author: T. Neidhart,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-06-15 12:20:22

También he tenido problemas similares al usar MethodHandle.Lookup en JDK 8 - 10, que se comportan de manera diferente. He blogueado 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 package-private 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 tanto con interfaces de acceso privado como con interfaces de acceso privado. Sin embargo, el enfoque anterior hace ilegal acceso reflectante a los internos de JDK, que ya no funcionarán en una futura versión 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 compruebe si su código se está ejecutando en JDK 8 o en un JDK posterior y estará bien. Hasta que no estés:)

 5
Author: Lukas Eder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-28 10:45:42

Si todo lo que tiene es una interfaz, y todo lo que tiene acceso a un objeto de clase es una interfaz que extiende su interfaz base, y desea llamar al método predeterminado sin una instancia real de una clase que implemente 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 los MethodHandles.Búsqueda usando 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 permitirá el acceso privado a invokespecial. Entonces invoca el método en el proxy falso target que hiciste anteriormente.

lookupConstructor.newInstance(exampleInterface,
        MethodHandles.Lookup.PRIVATE)
        .unreflectSpecial(method, declaringClass)
        .bindTo(target)
        .invokeWithArguments(args);
 2
Author: junkgui,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-25 17:18:01

Uso:

Object result = MethodHandles.lookup()
    .in(method.getDeclaringClass())
    .unreflectSpecial(method, method.getDeclaringClass())
    .bindTo(target)
    .invokeWithArguments();
 1
Author: Krzysztof Krasoń,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-06-14 12:48:34