How to explicitly invoke default method from a dynamic Proxy?

前端 未结 6 809
一向
一向 2020-12-09 15:33

Since Java 8 interfaces could have default methods. I know how to invoke the method explicitly from the implementing method, i.e. (see Explicitly calling a default method i

相关标签:
6条回答
  • 2020-12-09 15:48

    Use:

    Object result = MethodHandles.lookup()
        .in(method.getDeclaringClass())
        .unreflectSpecial(method, method.getDeclaringClass())
        .bindTo(target)
        .invokeWithArguments();
    
    0 讨论(0)
  • 2020-12-09 15:58

    If all you have is an interface, and all you have access to is a class object is an interface that extends your base interface, and you want to call the default method without a real instance of a class that implements the interface, you can:

    Object target = Proxy.newProxyInstance(classLoader,
          new Class[]{exampleInterface}, (Object p, Method m, Object[] a) -> null);
    

    Create an instance of the interface, and then construct the MethodHandles.Lookup using reflection:

    Constructor<MethodHandles.Lookup> lookupConstructor = 
        MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, Integer.TYPE);
    if (!lookupConstructor.isAccessible()) {
        lookupConstructor.setAccessible(true);
    }
    

    And then use that lookupConstructor to create a new instance of your interface that will allow private access to invokespecial. Then invoke the method on the fake proxy target you made earlier.

    lookupConstructor.newInstance(exampleInterface,
            MethodHandles.Lookup.PRIVATE)
            .unreflectSpecial(method, declaringClass)
            .bindTo(target)
            .invokeWithArguments(args);
    
    0 讨论(0)
  • 2020-12-09 16:00

    We can see how spring process default method.

    1. try invoke public method MethodHandles.privateLookupIn(Class,Lookup) first. This should success on jdk9+.
    2. try create a Lookup with package private constructor MethodHandles.Lookup(Class).
    3. fallback to MethodHandles.lookup().findSpecial(...)

    https://github.com/spring-projects/spring-data-commons/blob/2.1.8.RELEASE/src/main/java/org/springframework/data/projection/DefaultMethodInvokingMethodInterceptor.java

    0 讨论(0)
  • 2020-12-09 16:02

    T. Neidhart answer almost worked but I got the java.lang.IllegalAccessException: no private access for invokespecial

    Changing to use MethodHandles.privateLookup() solved it

    return MethodHandles.privateLookupIn(clazz,MethodHandles.lookup())
                            .in(clazz)
                            .unreflectSpecial(method, clazz)
                            .bindTo(proxy)
                            .invokeWithArguments(args);
    

    Here's a full example, the idea is that a user that extends a provided IMap can access nested nested map's with he's custom interface

    interface IMap {
        Object get(String key);
    
        default <T> T getAsAny(String key){
            return (T)get(key);
        }
    
    
        default <T extends IMap> T getNestedAs(String key, Class<T> clazz) {
            Map<String,Object> nested = getAsAny(key);
            return (T)Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz},  (proxy, method, args) -> {
                        if (method.getName().equals("get")){
                            return nested.get(args[0]);
                        }
                        return MethodHandles.privateLookupIn(clazz, MethodHandles.lookup())
                                .in(clazz)
                                .unreflectSpecial(method, clazz)
                                .bindTo(proxy)
                                .invokeWithArguments(args);
                    }
            );
        }
    }
    
    interface IMyMap extends IMap{
    
        default Integer getAsInt(String key){
            return getAsAny(key);
        }
        default IMyMap getNested(String key){
            return getNestedAs(key,IMyMap.class);
        }
    }
    
    @Test
    public void test(){
        var data =Map.of("strKey","strValue", "nstKey", Map.of("intKey",42));
        IMyMap base = data::get;
    
        IMyMap myMap = base.getNested("nstKey");
        System.out.println( myMap.getAsInt("intKey"));
    }
    
    0 讨论(0)
  • 2020-12-09 16:08

    If you use a concrete impl class as lookupClass and caller for the invokeSpecial it should correctly invoke the default implementation of the interface (no hack for private access needed):

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

    This of course only works if you have a reference to a concrete object implementing the interface.

    Edit: this solution will only work if the class in question (Example in the code above), is private accessible from the caller code, e.g. an anonymous inner class.

    The current implementation of the MethodHandles/Lookup class will not allow to call invokeSpecial on any class that is not private accessible from the current caller class. There are various work-arounds available, but all of them require the use of reflection to make constructors/methods accessible, which will probably fail in case a SecurityManager is installed.

    0 讨论(0)
  • 2020-12-09 16:10

    I've been troubled by similar issues as well when using MethodHandle.Lookup in JDK 8 - 10, which behave differently. I've blogged about the correct solution here in detail.

    This approach works in Java 8

    In Java 8, the ideal approach uses a hack that accesses a package-private constructor from 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(args);
                    return null;
                }
            );
    
            duck.quack();
        }
    }
    

    This is the only approach that works with both private-accessible and private-inaccessible interfaces. However, the above approach does illegal reflective access to JDK internals, which will no longer work in a future JDK version, or if --illegal-access=deny is specified on the JVM.

    This approach works on Java 9 and 10, but not 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(args);
                    return null;
                }
            );
    
            duck.quack();
        }
    }
    

    Solution

    Simply implement both of the above solutions and check if your code is running on JDK 8 or on a later JDK and you'll be fine. Until you're not :)

    0 讨论(0)
提交回复
热议问题