Java8 dynamic proxy and default methods

后端 未结 4 1225
北荒
北荒 2021-01-01 20:43

Having a dynamic proxy for an interface with default methods, how do I invoke a default method? By using something like defaultmethod.invoke(this, ...) you just

相关标签:
4条回答
  • 2021-01-01 21:04

    This is annoyingly stupid counter-intuitive behaviour, which I assert is a bug in method#invoke(Object,Object[]), because you can't keep things simple in an InvocationHandler, like:

    if (method.isDefault())
        method.invoke(proxy, args);
    else
        method.invoke(target, args); // to call a wrapped object
    

    So have to do a special lookup for a MethodHandle, and bind to proxy, to call, it.

    I refined the McDowell provided code as follows (simplified):

    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
          }
     }
    

    facadeInterface is the interface being proxied, which declares the default method, it will probably be possible to use super-interface default methods too.

    Non-toy code should do this lookup before invoke is called, or at least cache the MethodHandle.

    0 讨论(0)
  • 2021-01-01 21:07

    I wrote up a blog entry detailing the different approaches that must be used for Java 8 and 9+: http://netomi.github.io/2020/04/17/default-methods.html

    It includes code from the spring framework to handle the different cases in a clean and efficient way.

    0 讨论(0)
  • 2021-01-01 21:08

    The accepted answer uses setAccessible(true) to break into MethodHandles.Lookup, something that is restricted in Java 9 and beyond. This mail describes a JDK change that works for Java 9 or later.

    It is possible to get this to work on Java 8 (and later) if you can get the writer of the interface to call your utility with an instance of MethodHandles.Lookup created in the interface (so it gets the permission to access the default methods of the interface):

    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);
      }
    }
    

    This approach won't handle all Java 8 use cases, but it did handle mine.

    0 讨论(0)
  • 2021-01-01 21:26

    You can use the MethodHandles type in your InvocationHandler. This code is copied from 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);
    }
    
    0 讨论(0)
提交回复
热议问题