在对Java中的默认方法执行反射式访问时,Google似乎让我们失望了。The solutions presented on Stack Overflow例如,它似乎只在某些情况下工作,而不是在所有Java版本上工作。
本文将说明通过反射调用接口默认方法的不同方法,例如,代理可能需要反射。
如果你不耐烦了,all the access methods exposed in this blog are available in this gist这个问题在我们的图书馆里也得到了解决jOOR。
有用的java.lang.reflect.Proxy
API已经出现了一段时间。我们可以做一些很酷的事情,比如:
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
在本例中,我们创建了一个代理实例,它实现了Duck
API通过InvocationHandler
,它本质上只是一个lambda,每次调用Duck
。
有趣的一点是当我们想要在Duck
并将调用委托给默认方法:
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
没什么帮助。
所以,最初的谷歌搜索出现了results that indicate we need to use the 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
's内部通过反射:
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
我们在JDK8上得到了这一点。那么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
哎呀。That’s what happens by default。如果我们用--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)
Jigsaw项目的目标之一就是不允许此类黑客继续存在。那么,有什么更好的解决办法呢?这个?
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)可以在Java8上工作,但不能在9或10上工作,也有一个解决方案可以在Java9和10上工作,但不能在Java8上工作。
到目前为止,我只是尝试在不同的JDK上运行不同的东西。下面的类尝试所有组合。It’s also available in this gist here。
用JDK9或10编译它(因为它也尝试使用JDK9+API:MethodHandles.privateLookupIn()
),但是使用以下命令编译它,并且您还可以在JDK8:
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
... same result as in Java 9
把这件事做对有点棘手。
Lookup.findSpecial()
(在Java 8中不起作用)或新的MethodHandles.privateLookupIn()
(在Java8中不存在),我怀疑它的用例是允许调用私有接口方法,这些方法是在JDK9中添加的公平地说,这有点乱。这里合适的模因是:
根据Rafael Winterhalter的说法,“真正的”修复应该是修改代理API:
我不确定这是否能解决所有的问题,但是实现者绝对不应该担心上面所有的问题。
如果您使用jOOR (our reflection library, check it out here),即将发布的0.9.8版本将包含一个修复程序:
https://github.com/jOOQ/jOOR/issues/49
修复程序只是使用了Java8中不安全的反射方法,或者使用了MethodHandles.privateLookupIn()
方法。然后可以写:
Reflect.on(new Object()).as(PrivateAccessible.class).quack();
Reflect.on(new Object()).as(PrivateInaccessible.class).quack();