如何通过反射调用Java 8默认方法

19

假设有这样一个简单的Java 8接口,类似于"Hello World",如何通过反射调用它的hello()方法?

public interface Hello {
    default String hello() {
        return "Hello";
    }
}

相关:https://dev59.com/kVoU5IYBdhLWcg3wHERo 如@Rudziankoŭ所述,已被标记为重复。 - earcam
5个回答

17

很遗憾,在所有JDK8、9、10上运行的理想解决方案似乎并不存在,它们的行为不同。 在修复jOOR中的问题时,我遇到了一些问题我在这里也详细介绍了正确的解决方案

这种方法适用于Java 8

在Java 8中,理想的方法使用从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();
    }
}

这是唯一适用于私有可访问和私有不可访问接口的方法。然而,上述方法会非法反射访问JDK内部,这在未来的JDK版本中将不再起作用,或者如果在JVM上指定了--illegal-access=deny

这种方法适用于Java 9和10,但不适用于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();
    }
}

解决方案

只需实现上述两种解决方案,检查您的代码是否在JDK 8或更高版本上运行,就可以了。直到你不行为止 :)


我认为这个解决方案并不完整,因为在一般情况下,类引用(Duck.class)作为参数传递。该类可能是一个具体类,它从实现的接口或超类继承了默认方法,或者从继承了默认方法的超接口或从继承了方法的超类继承了默认方法。您必须遍历所有超类,收集唯一的已实现接口,然后找到超级接口的传递闭包,然后尝试每个接口。 - Luke Hutchison
@LukeHutchison。使用案例是传递保存默认方法本身的接口。这就是OP想要的。我不确定你在寻找什么。 - Lukas Eder

16

您可以使用MethodHandles达到这个目的:

import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ReflectiveDefaultMethodCallExample {

    static interface Hello {
        default String hello() {
            return "Hello";
        }
    }

    public static void main(String[] args) throws Throwable{

        Hello target =
                //new Hello(){};
                (Hello)Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),new Class[]{Hello.class}, (Object proxy, Method method, Object[] arguments) -> null);
        Method method = Hello.class.getMethod("hello");

        Object result = MethodHandles.lookup()
                                     .in(method.getDeclaringClass())
                                     .unreflectSpecial(method,method.getDeclaringClass())
                                     .bindTo(target)
                                     .invokeWithArguments();
        System.out.println(result); //Hello
    }
}

2
不错的想法,因为它给出了一个提示,说明InvocationHandler如何委托给默认方法。在其他场景中,这可能非常有用。 - Holger
4
除了在查找句柄的文件之外定义的任何接口都将导致失败,这是正确的。我使用了以下修改: final Field field = Lookup.class.getDeclaredField("IMPL_LOOKUP"); field.setAccessible(true); final Lookup lookup = (Lookup) field.get(null); final Object value = lookup .unreflectSpecial(method, method.getDeclaringClass()) .bindTo(t) .invokeWithArguments(); - Ajax
2
这篇文章的评论区也有其他解决方法 https://rmannibucau.wordpress.com/2014/03/27/java-8-default-interface-methods-and-jdk-dynamic-proxies/,最好的方法可能是获取Lookup类的构造函数,这样您就可以创建一个具有私有访问权限的查找器,而不是像我上面所做的那样访问完全授权的IMPL_LOOKUP字段。 - Ajax
只是为了澄清一下... MethodHandles 对于谁可以访问哪些方法有反射保护,因此您需要从 Lookup 类中获取通用的 Lookup 对象,而不是使用 MethodHandles.lookup 提供的对象。 - Ajax

7

非常感谢Lukas。这里是他的答案,包括Java 8与9+的检查和对非void返回和参数的支持。请务必为他的答案点个赞。

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;

public class ThanksLukas implements InvocationHandler {
    @Override
    public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {

        if (method.isDefault()) {
            final float version = Float.parseFloat(System.getProperty("java.class.version"));
            if (version <= 52) {
                final Constructor<Lookup> constructor = Lookup.class.getDeclaredConstructor(Class.class);
                constructor.setAccessible(true);

                final Class<?> clazz = method.getDeclaringClass();
                return constructor.newInstance(clazz)
                        .in(clazz)
                        .unreflectSpecial(method, clazz)
                        .bindTo(proxy)
                        .invokeWithArguments(args);
            } else {
                return MethodHandles.lookup()
                        .findSpecial(
                                method.getDeclaringClass(),
                                method.getName(),
                                MethodType.methodType(method.getReturnType(), new Class[0]),
                                method.getDeclaringClass()
                        ).bindTo(proxy)
                        .invokeWithArguments(args);
            }
        }

        // your regular proxy fun here

4
您不能直接调用它,因为您需要一个实现类的实例。而且为此,您需要一个实现类。default方法不是static方法,您也无法创建接口的实例。
所以,假设您有一个实现类:
class HelloImpl implements Hello {  }

您可以这样调用该方法:

Class<HelloImpl> clazz = HelloImpl.class;
Method method = clazz.getMethod("hello");
System.out.println(method.invoke(new HelloImpl()));  // Prints "Hello"

我在问题上应该更加精确:如何通过反射实例化它并调用它的hello()方法 - 两者都要 - thekid
2
@thekid 你不能实例化一个接口。你需要一个实现类,我在答案中已经展示了。 - Rohit Jain

3

我已经使用sun.misc.ProxyGenerator中的代码通过反射地创建类似上面的接口实例的方式找到了解决方案。现在,我可以这样编写:

found a solution

Class<?> clazz = Class.forName("Hello");
Object instance;

if (clazz.isInterface()) {
    instance = new InterfaceInstance(clazz).defineClass().newInstance();
} else {
    instance = clazz.newInstance();
}

return clazz.getMethod("hello").invoke(instance);

...但那相当丑陋。


这必须很丑陋,否则就会有人想使用它! - Michal Gruca
肯定比必要的工作多。可以通过MethodHandles进行访问,这不需要大量依赖项或过多的运行时工作。 - Ajax

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接