译者注:CUBA框架的开发催生了大量的研发项目。 在这样一个项目的过程中,事实证明,我们需要从代理类调用默认接口方法。 我们偶然发现了一篇非常有用的文章,在我看来,其中介绍的经验至少会很有趣,并且对广大开发人员来说非常有用。通过反射访问Java中的默认接口方法时,google并没有太大帮助。 例如,
StackOverflow上的
解决方案仅在某些情况下适用,而不适用于所有Java版本。
本文将讨论通过反射调用默认接口方法的各种方法,例如在创建代理类时,这可能是必需的。
TL; DR如果您迫不及待,那么
此链接中提供
了本文介绍的所有调用默认方法的方法,并且该问题已在我们的
jOOR库中解决。
使用默认方法代理接口
有用的API java.lang.reflect.Proxy已经存在很长时间了,我们可以用它做一些很酷的事情,例如:
import java.lang.reflect.Proxy; public class ProxyDemo { interface Duck { void quack(); } public static void main(String[] a) { Duck duck = (Duck) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class[] { Duck.class }, (proxy, method, args) -> { System.out.println("Quack"); return null; } ); duck.quack(); } }
此代码仅输出:
Quack
在此示例中,我们创建了一个代理实例,该实例使用
InvocationHandler来实现Duck API,该代理基本上只是为Duck接口的每个方法调用的lambda。
当我们要向接口添加方法实现并委托对该方法的调用时,有趣的部分将开始:
interface Duck { default void quack() { System.out.println("Quack"); } }
您很可能希望编写以下代码:
import java.lang.reflect.Proxy; public class ProxyDemo { interface Duck { default void quack() { System.out.println("Quack"); } } public static void main(String[] a) { Duck duck = (Duck) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class[] { Duck.class }, (proxy, method, args) -> { method.invoke(proxy); return null; } ); duck.quack(); } }
但这只会生成大量的嵌套异常(并且与在接口中调用方法的实现无关,因此完全禁止这样做):
Exception in thread "main" java.lang.reflect.UndeclaredThrowableException at $Proxy0.quack(Unknown Source) at ProxyDemo.main(ProxyDemo.java:20) Caused by: java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at ProxyDemo.lambda$0(ProxyDemo.java:15) ... 2 more Caused by: java.lang.reflect.UndeclaredThrowableException at $Proxy0.quack(Unknown Source) ... 7 more Caused by: java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at ProxyDemo.lambda$0(ProxyDemo.java:15) ... 8 more Caused by: java.lang.reflect.UndeclaredThrowableException at $Proxy0.quack(Unknown Source) ... 13 more Caused by: java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at ProxyDemo.lambda$0(ProxyDemo.java:15) ... 14 more Caused by: java.lang.reflect.UndeclaredThrowableException at $Proxy0.quack(Unknown Source) ... 19 more ... ... ... goes on forever
不是很有帮助。
使用方法句柄API
因此,谷歌搜索告诉我们,
我们需要使用MethodHandles API 。 好吧,让我们尝试!
import java.lang.invoke.MethodHandles; import java.lang.reflect.Proxy; public class ProxyDemo { interface Duck { default void quack() { System.out.println("Quack"); } } public static void main(String[] a) { Duck duck = (Duck) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class[] { Duck.class }, (proxy, method, args) -> { MethodHandles .lookup() .in(Duck.class) .unreflectSpecial(method, Duck.class) .bindTo(proxy) .invokeWithArguments(); return null; } ); duck.quack(); } }
太酷了,看起来不错!
Quack
...但是没有。
调用具有非私有访问权限的接口方法
上面示例中的界面经过精心制作,因此调用代码可以对其进行私有访问,即 接口嵌套在调用类中。 但是,如果我们有一个非嵌套的接口怎么办?
import java.lang.invoke.MethodHandles; 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() .in(Duck.class) .unreflectSpecial(method, Duck.class) .bindTo(proxy) .invokeWithArguments(); return null; } ); duck.quack(); } }
几乎相同的代码不再起作用。 获取IllegalAccessException:
Exception in thread "main" java.lang.reflect.UndeclaredThrowableException at $Proxy0.quack(Unknown Source) at ProxyDemo.main(ProxyDemo.java:26) Caused by: java.lang.IllegalAccessException: no private access for invokespecial: interface Duck, from Duck/package at java.lang.invoke.MemberName.makeAccessException(MemberName.java:850) at java.lang.invoke.MethodHandles$Lookup.checkSpecialCaller(MethodHandles.java:1572) at java.lang.invoke.MethodHandles$Lookup.unreflectSpecial(MethodHandles.java:1231) at ProxyDemo.lambda$0(ProxyDemo.java:19) ... 2 more
胡说八道。 如果您用谷歌搜索它,则可以找到以下解决方案,该解决方案通过反射来访问
MethodHandles.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(); } }
而且,加油打气,我们得到:
Quack
我们设法在JDK 8上做到这一点。JDK 9或10如何?
WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by ProxyDemo (file:/C:/Users/lukas/workspace/playground/target/classes/) to constructor java.lang.invoke.MethodHandles$Lookup(java.lang.Class) WARNING: Please consider reporting this to the maintainers of ProxyDemo WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release Quack
套鞋。 这是
默认情况下发生
的情况 。 如果我们使用
--illegal-access=deny
标志运行程序:
java --illegal-access=deny ProxyDemo
好吧,那么我们得到(正确的是!):
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make java.lang.invoke.MethodHandles$Lookup(java.lang.Class) accessible: module java.base does not "opens java.lang.invoke" to unnamed module @357246de at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:337) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:281) at java.base/java.lang.reflect.Constructor.checkCanSetAccessible(Constructor.java:192) at java.base/java.lang.reflect.Constructor.setAccessible(Constructor.java:185) at ProxyDemo.lambda$0(ProxyDemo.java:18) at $Proxy0.quack(Unknown Source) at ProxyDemo.main(ProxyDemo.java:28)
拼图项目的目标之一就是防止此类黑客入侵。 那么,哪种解决方案更好? 是吗
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(); } }
Quack
太好了,这可以在Java 9和10中使用,但是Java 8呢?
Exception in thread "main" java.lang.reflect.UndeclaredThrowableException at $Proxy0.quack(Unknown Source) at ProxyDemo.main(ProxyDemo.java:25) Caused by: java.lang.IllegalAccessException: no private access for invokespecial: interface Duck, from ProxyDemo at java.lang.invoke.MemberName.makeAccessException(MemberName.java:850) at java.lang.invoke.MethodHandles$Lookup.checkSpecialCaller(MethodHandles.java:1572) at java.lang.invoke.MethodHandles$Lookup.findSpecial(MethodHandles.java:1002) at ProxyDemo.lambda$0(ProxyDemo.java:18) ... 2 more
你在开玩笑吗?
因此,我们有一个解决方案(hack)可以在Java 8中运行,但不能在9和10中运行,还有一个解决方案可以在9和10中运行,但不能在8中运行
更深入的研究
好吧,我只是尝试在不同的JDK上运行不同的代码。 下一课尝试上述所有组合。 也可以
在这里作为GIST使用。
使用JDK 9或10编译代码(因为需要JDK 9+ API:
MethodHandles.privateLookupIn() ),但是您需要
使用以下命令对其进行编译以在JDK 8上运行该类:
javac -source 1.8 -target 1.8 CallDefaultMethodThroughReflection.java
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles.Lookup; import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Proxy; interface PrivateInaccessible { default void quack() { System.out.println(" -> PrivateInaccessible.quack()"); } } public class CallDefaultMethodThroughReflection { interface PrivateAccessible { default void quack() { System.out.println(" -> PrivateAccessible.quack()"); } } public static void main(String[] args) { System.out.println("PrivateAccessible"); System.out.println("-----------------"); System.out.println(); proxy(PrivateAccessible.class).quack(); System.out.println(); System.out.println("PrivateInaccessible"); System.out.println("-------------------"); System.out.println(); proxy(PrivateInaccessible.class).quack(); } private static void quack(Lookup lookup, Class<?> type, Object proxy) { System.out.println("Lookup.in(type).unreflectSpecial(...)"); try { lookup.in(type) .unreflectSpecial(type.getMethod("quack"), type) .bindTo(proxy) .invokeWithArguments(); } catch (Throwable e) { System.out.println(" -> " + e.getClass() + ": " + e.getMessage()); } System.out.println("Lookup.findSpecial(...)"); try { lookup.findSpecial(type, "quack", MethodType.methodType(void.class, new Class[0]), type) .bindTo(proxy) .invokeWithArguments(); } catch (Throwable e) { System.out.println(" -> " + e.getClass() + ": " + e.getMessage()); } } @SuppressWarnings("unchecked") private static <T> T proxy(Class<T> type) { return (T) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class[] { type }, (Object proxy, Method method, Object[] arguments) -> { System.out.println("MethodHandles.lookup()"); quack(MethodHandles.lookup(), type, proxy); try { System.out.println(); System.out.println("Lookup(Class)"); Constructor<Lookup> constructor = Lookup.class.getDeclaredConstructor(Class.class); constructor.setAccessible(true); constructor.newInstance(type); quack(constructor.newInstance(type), type, proxy); } catch (Exception e) { System.out.println(" -> " + e.getClass() + ": " + e.getMessage()); } try { System.out.println(); System.out.println("MethodHandles.privateLookupIn()"); quack(MethodHandles.privateLookupIn(type, MethodHandles.lookup()), type, proxy); } catch (Error e) { System.out.println(" -> " + e.getClass() + ": " + e.getMessage()); } return null; } ); } }
上面程序的输出:
Java 8 $ java -version java version "1.8.0_141" Java(TM) SE Runtime Environment (build 1.8.0_141-b15) Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode) $ java CallDefaultMethodThroughReflection PrivateAccessible ----------------- MethodHandles.lookup() Lookup.in(type).unreflectSpecial(...) -> PrivateAccessible.quack() Lookup.findSpecial(...) -> class java.lang.IllegalAccessException: no private access for invokespecial: interface CallDefaultMethodThroughReflection$PrivateAccessible, from CallDefaultMethodThroughReflection Lookup(Class) Lookup.in(type).unreflectSpecial(...) -> PrivateAccessible.quack() Lookup.findSpecial(...) -> PrivateAccessible.quack() MethodHandles.privateLookupIn() -> class java.lang.NoSuchMethodError: java.lang.invoke.MethodHandles.privateLookupIn(Ljava/lang/Class;Ljava/lang/invoke/MethodHandles$Lookup;)Ljava/lang/invoke/MethodHandles$Lookup; PrivateInaccessible ------------------- MethodHandles.lookup() Lookup.in(type).unreflectSpecial(...) -> class java.lang.IllegalAccessException: no private access for invokespecial: interface PrivateInaccessible, from PrivateInaccessible/package Lookup.findSpecial(...) -> class java.lang.IllegalAccessException: no private access for invokespecial: interface PrivateInaccessible, from CallDefaultMethodThroughReflection Lookup(Class) Lookup.in(type).unreflectSpecial(...) -> PrivateInaccessible.quack() Lookup.findSpecial(...) -> PrivateInaccessible.quack() MethodHandles.privateLookupIn() -> class java.lang.NoSuchMethodError: java.lang.invoke.MethodHandles.privateLookupIn(Ljava/lang/Class;Ljava/lang/invoke/MethodHandles$Lookup;)Ljava/lang/invoke/MethodHandles$Lookup;
Java 9 $ java -version java version "9.0.4" Java(TM) SE Runtime Environment (build 9.0.4+11) Java HotSpot(TM) 64-Bit Server VM (build 9.0.4+11, mixed mode) $ java --illegal-access=deny CallDefaultMethodThroughReflection PrivateAccessible ----------------- MethodHandles.lookup() Lookup.in(type).unreflectSpecial(...) -> PrivateAccessible.quack() Lookup.findSpecial(...) -> PrivateAccessible.quack() Lookup(Class) -> class java.lang.reflect.InaccessibleObjectException: Unable to make java.lang.invoke.MethodHandles$Lookup(java.lang.Class) accessible: module java.base does not "opens java.lang.invoke" to unnamed module @30c7da1e MethodHandles.privateLookupIn() Lookup.in(type).unreflectSpecial(...) -> PrivateAccessible.quack() Lookup.findSpecial(...) -> PrivateAccessible.quack() PrivateInaccessible ------------------- MethodHandles.lookup() Lookup.in(type).unreflectSpecial(...) -> class java.lang.IllegalAccessException: no private access for invokespecial: interface PrivateInaccessible, from PrivateInaccessible/package (unnamed module @30c7da1e) Lookup.findSpecial(...) -> PrivateInaccessible.quack() Lookup(Class) -> class java.lang.reflect.InaccessibleObjectException: Unable to make java.lang.invoke.MethodHandles$Lookup(java.lang.Class) accessible: module java.base does not "opens java.lang.invoke" to unnamed module @30c7da1e MethodHandles.privateLookupIn() Lookup.in(type).unreflectSpecial(...) -> PrivateInaccessible.quack() Lookup.findSpecial(...) -> PrivateInaccessible.quack()
Java 10 $ java -version java version "10" 2018-03-20 Java(TM) SE Runtime Environment 18.3 (build 10+46) Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10+46, mixed mode) $ java --illegal-access=deny CallDefaultMethodThroughReflection ... , Java 9
结论
要了解所有这些,有些复杂。
- 在Java 8中,最好的工作方法是通过访问
Lookup
类的程序包专用构造函数来侵入JDK内部的黑客。 这是通过任何类的私有访问和非私有访问以一致的方式调用接口方法的唯一方法。 - 在Java 9和10中,最好的方法是使用
Lookup.findSpecial()
(在Java 8中不起作用)或MethodHandles.privateLookupIn()
(该方法在Java 8中不存在)。 如果接口在另一个模块中,则应使用后一种方法。 该模块应提供用于外部呼叫的接口。
老实说,这有点令人困惑。 适用的模因:

Rafael Winterhalter(ByteBuddy的作者)表示,“真正的”修复将在代理API的修订版中进行:

笔译卢卡斯·埃德(Lukas Eder) :“您不知道您决定不再允许它的原因了吗? 还是只是错过了(很可能没有)?”
拉斐尔·温特豪德(Rafael Winterhalter) :“没有理由。 这是MethodHandle中的Lookup类的Java安全模型的副作用。 理想情况下,代理接口应该提供这样的Lookup作为参数( 构造函数-大约Per。 ),但是他们不考虑这一点。 我没有为类文件转换API提供类似的扩展。”
我不确定这是否可以解决所有问题,但您确实需要确保开发人员不必担心上述所有问题。
很显然,本文并不完整,例如,没有测试如果从另一个模块导入Duck时这些方法是否行得通:

笔译JOOQ : 标题和文章链接
拉斐尔·温特豪德(Rafael Winterhalter) :“您是否尝试过将Duck放入一个可以导出但不打开接口包的模块中? 我敢打赌,如果您使用模块路径,则Java 9+的解决方案将无法正常工作。”
...这将是下一篇文章的主题。
使用jOOR
如果您使用jOOR(我们的反射API库位于
此处 ),那么0.9.8版将包含针对此的修复程序:
github.com/jOOQ/jOOR/issues/49Fix仅在Java 8或Java 9+的MethodHandles.privateLookupIn()中使用了Reflection API hack方法。 您可以写:
Reflect.on(new Object()).as(PrivateAccessible.class).quack(); Reflect.on(new Object()).as(PrivateInaccessible.class).quack();