如何从动态代理显式调用默认方法?

由于Java 8接口可以有默认方法。 我知道如何从实现方法中显式调用该方法,即(请参阅在Java中显式调用默认方法 )

但是,如何在代理上使用reflection显式调用默认方法?

例:

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

编辑:我知道有一个类似的问题在如何反复调用Java 8默认方法 ,但这并没有解决我的问题有两个原因:

  • 该问题中描述的问题针对如何通过reflection一般调用它 – 因此没有区分默认和重写方法 – 这很简单,你只需要一个实例。
  • 其中一个答案 – 使用方法句柄 – 只能使用令人讨厌的黑客(imho),例如将访问修饰符更改为查找类的字段,这类似于“解决方案”,如下所示: 使用Javareflection更改私有静态最终字段 : 很高兴知道这是可能的,但我不会在生产中使用它 – 我正在寻找一种“官方”方式来实现它。

IllegalAccessException中抛出IllegalAccessException

 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) 

如果你使用一个具体的impl类作为lookupClass和invokeSpecial的调用者,它应该正确地调用接口的默认实现(不需要私有访问的hack):

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

这当然只有在您对实现接口的具体对象的引用时才有效。

编辑:此解决方案仅在有问题的类(上面的代码中的示例)可以从调用者代码(例如匿名内部类)访问时才起作用。

MethodHandles / Lookup类的当前实现将不允许在任何不能从当前调用者类访问私有的类上调用invokeSpecial。 有各种可用的解决方法,但所有这些都需要使用reflection来使构造函数/方法可访问,如果安装了SecurityManager,这可能会失败。

在JDK 8 – 10中使用MethodHandle.Lookup时,我也遇到了类似的问题。 我在这里详细介绍了正确的解决方案 。

这种方法适用于Java 8

在Java 8中,理想的方法是使用从Lookup访问包私有构造函数的hack:

 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 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内部进行非法reflection访问,这将在将来的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上运行还是在以后的JDK上运行,您就可以了。 直到你不是:)

如果您拥有的只是一个接口,并且您可以访问的是一个类对象是一个扩展您的基本接口的接口,并且您希望在没有实现该接口的类的实际实例的情况下调用默认方法,您可以:

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

创建接口的实例,然后使用reflection构造MethodHandles.Lookup:

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

然后使用lookupConstructor创建一个新的接口实例,允许私有访问invokespecial 。 然后在您之前创建的伪代理target上调用该方法。

 lookupConstructor.newInstance(exampleInterface, MethodHandles.Lookup.PRIVATE) .unreflectSpecial(method, declaringClass) .bindTo(target) .invokeWithArguments(args); 

使用:

 Object result = MethodHandles.lookup() .in(method.getDeclaringClass()) .unreflectSpecial(method, method.getDeclaringClass()) .bindTo(target) .invokeWithArguments();