如何从动态代理显式调用默认方法?
How to explicitly invoke default method from a dynamic Proxy?
因为 Java 8 个接口可以有默认方法。
我知道如何从实现方法中显式调用该方法,即
(参见 Explicitly calling a default method in Java)
但是我如何显式调用默认方法,例如在代理上使用反射?
示例:
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<Object, Object[], Object>> 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();
}
}
编辑: 我知道 How do I invoke Java 8 default methods refletively 中有人问过类似的问题,但这并没有解决我的问题,原因有二:
- 该问题中描述的问题旨在说明如何通过反射调用它一般 - 因此默认方法和覆盖方法之间没有区别 - 这很简单,你只需要实例。
- 答案之一 - 使用方法句柄 - 只适用于讨厌的 hack(恕我直言),例如将访问修饰符更改为查找字段 class,这与 "solutions" 的类别相同this: Change private static final field using Java reflection: 很高兴知道这是可能的,但我不会在生产中使用它 - 我正在寻找一种 "official" 方法来做到这一点.
IllegalAccessException
被扔进 unreflectSpecial
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[=11=](Example.java:30)
at example.Example$$Lambda/1342443276.invoke(Unknown Source)
使用:
Object result = MethodHandles.lookup()
.in(method.getDeclaringClass())
.unreflectSpecial(method, method.getDeclaringClass())
.bindTo(target)
.invokeWithArguments();
如果您使用具体实现 class 作为 lookupClass 和 invokeSpecial 的调用者,它应该正确调用接口的默认实现(不需要私有访问的 hack):
Example target = new Example();
...
Class targetClass = target.getClass();
return MethodHandles.lookup()
.in(targetClass)
.unreflectSpecial(method, targetClass)
.bindTo(target)
.invokeWithArguments();
这当然只有在您引用实现该接口的具体对象时才有效。
编辑:此解决方案仅在有问题的 class(上面代码中的示例)可从调用者代码访问时才有效,例如匿名内部 class.
MethodHandles/Lookup class 的当前实现将不允许在当前调用者 class 无法私有访问的任何 class 上调用 invokeSpecial。有各种可用的解决方法,但所有这些方法都需要使用反射来使 constructors/methods 可访问,如果安装了 SecurityManager,这可能会失败。
如果您只有一个接口,并且您只能访问一个 class 对象是一个扩展您的基接口的接口,并且您想在没有真正实例的情况下调用默认方法class实现接口,可以:
Object target = Proxy.newProxyInstance(classLoader,
new Class[]{exampleInterface}, (Object p, Method m, Object[] a) -> null);
创建接口实例,然后使用反射构造MethodHandles.Lookup:
Constructor<MethodHandles.Lookup> 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);
在 JDK 8 - 10 中使用 MethodHandle.Lookup
时,我也遇到过类似的问题,它们的行为不同。 I've blogged about the correct solution here in detail.
这种方法适用于 Java 8
在 Java 8 中,理想的方法是使用 hack 从 Lookup
:
访问 package-private 构造函数
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(args);
return null;
}
);
duck.quack();
}
}
这是唯一适用于 private-accessible 和 private-inaccessible 接口的方法。但是,上述方法对 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(args);
return null;
}
);
duck.quack();
}
}
解决方案
只需实施上述两种解决方案并检查您的代码是否在 JDK 8 或更高版本的 JDK 上 运行 就可以了。直到你不:)
我们可以看到 spring 如何处理默认方法。
- 首先尝试调用 public 方法
MethodHandles.privateLookupIn(Class,Lookup)
。这应该在 jdk9+ 上成功。
- 尝试使用包私有构造函数创建查找
MethodHandles.Lookup(Class)
。
- 回退到 MethodHandles.lookup().findSpecial(...)
T。 Neidhart 的回答几乎奏效了,但我得到了 java.lang.IllegalAccessException: no private access for invokespecial
改为使用 MethodHandles.privateLookup() 解决了
return MethodHandles.privateLookupIn(clazz,MethodHandles.lookup())
.in(clazz)
.unreflectSpecial(method, clazz)
.bindTo(proxy)
.invokeWithArguments(args);
这是一个完整的示例,其想法是扩展提供的 IMap 的用户可以使用他的自定义界面访问嵌套的嵌套地图
interface IMap {
Object get(String key);
default <T> T getAsAny(String key){
return (T)get(key);
}
default <T extends IMap> T getNestedAs(String key, Class<T> clazz) {
Map<String,Object> nested = getAsAny(key);
return (T)Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, (proxy, method, args) -> {
if (method.getName().equals("get")){
return nested.get(args[0]);
}
return MethodHandles.privateLookupIn(clazz, MethodHandles.lookup())
.in(clazz)
.unreflectSpecial(method, clazz)
.bindTo(proxy)
.invokeWithArguments(args);
}
);
}
}
interface IMyMap extends IMap{
default Integer getAsInt(String key){
return getAsAny(key);
}
default IMyMap getNested(String key){
return getNestedAs(key,IMyMap.class);
}
}
@Test
public void test(){
var data =Map.of("strKey","strValue", "nstKey", Map.of("intKey",42));
IMyMap base = data::get;
IMyMap myMap = base.getNested("nstKey");
System.out.println( myMap.getAsInt("intKey"));
}
In Java 16(来自documentation,里面也有更复杂的例子):
Object proxy = Proxy.newProxyInstance(loader, new Class[] { A.class },
(o, m, params) -> {
if (m.isDefault()) {
// if it's a default method, invoke it
return InvocationHandler.invokeDefault(o, m, params);
}
});
}
Lukas 的回答适用于 Android 8+(早期版本没有默认方法)但依赖于私有 API,后者在后来的 Android 版本中被阻止。幸运的是,替代构造函数也可以工作,目前处于灰名单(不受支持)中。示例(用 Kotlin 编写)可以在这里看到。
@get:RequiresApi(26)
private val newLookup by lazy @TargetApi(26) {
MethodHandles.Lookup::class.java.getDeclaredConstructor(Class::class.java, Int::class.java).apply {
isAccessible = true
}
}
@RequiresApi(26)
fun InvocationHandler.invokeDefault(proxy: Any, method: Method, vararg args: Any?) =
newLookup.newInstance(method.declaringClass, 0xf) // ALL_MODES
.unreflectSpecial(method, method.declaringClass)
.bindTo(proxy)
.invokeWithArguments(*args)
因为 Java 8 个接口可以有默认方法。 我知道如何从实现方法中显式调用该方法,即 (参见 Explicitly calling a default method in Java)
但是我如何显式调用默认方法,例如在代理上使用反射?
示例:
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<Object, Object[], Object>> 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();
}
}
编辑: 我知道 How do I invoke Java 8 default methods refletively 中有人问过类似的问题,但这并没有解决我的问题,原因有二:
- 该问题中描述的问题旨在说明如何通过反射调用它一般 - 因此默认方法和覆盖方法之间没有区别 - 这很简单,你只需要实例。
- 答案之一 - 使用方法句柄 - 只适用于讨厌的 hack(恕我直言),例如将访问修饰符更改为查找字段 class,这与 "solutions" 的类别相同this: Change private static final field using Java reflection: 很高兴知道这是可能的,但我不会在生产中使用它 - 我正在寻找一种 "official" 方法来做到这一点.
IllegalAccessException
被扔进 unreflectSpecial
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[=11=](Example.java:30)
at example.Example$$Lambda/1342443276.invoke(Unknown Source)
使用:
Object result = MethodHandles.lookup()
.in(method.getDeclaringClass())
.unreflectSpecial(method, method.getDeclaringClass())
.bindTo(target)
.invokeWithArguments();
如果您使用具体实现 class 作为 lookupClass 和 invokeSpecial 的调用者,它应该正确调用接口的默认实现(不需要私有访问的 hack):
Example target = new Example();
...
Class targetClass = target.getClass();
return MethodHandles.lookup()
.in(targetClass)
.unreflectSpecial(method, targetClass)
.bindTo(target)
.invokeWithArguments();
这当然只有在您引用实现该接口的具体对象时才有效。
编辑:此解决方案仅在有问题的 class(上面代码中的示例)可从调用者代码访问时才有效,例如匿名内部 class.
MethodHandles/Lookup class 的当前实现将不允许在当前调用者 class 无法私有访问的任何 class 上调用 invokeSpecial。有各种可用的解决方法,但所有这些方法都需要使用反射来使 constructors/methods 可访问,如果安装了 SecurityManager,这可能会失败。
如果您只有一个接口,并且您只能访问一个 class 对象是一个扩展您的基接口的接口,并且您想在没有真正实例的情况下调用默认方法class实现接口,可以:
Object target = Proxy.newProxyInstance(classLoader,
new Class[]{exampleInterface}, (Object p, Method m, Object[] a) -> null);
创建接口实例,然后使用反射构造MethodHandles.Lookup:
Constructor<MethodHandles.Lookup> 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);
在 JDK 8 - 10 中使用 MethodHandle.Lookup
时,我也遇到过类似的问题,它们的行为不同。 I've blogged about the correct solution here in detail.
这种方法适用于 Java 8
在 Java 8 中,理想的方法是使用 hack 从 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(args);
return null;
}
);
duck.quack();
}
}
这是唯一适用于 private-accessible 和 private-inaccessible 接口的方法。但是,上述方法对 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(args);
return null;
}
);
duck.quack();
}
}
解决方案
只需实施上述两种解决方案并检查您的代码是否在 JDK 8 或更高版本的 JDK 上 运行 就可以了。直到你不:)
我们可以看到 spring 如何处理默认方法。
- 首先尝试调用 public 方法
MethodHandles.privateLookupIn(Class,Lookup)
。这应该在 jdk9+ 上成功。 - 尝试使用包私有构造函数创建查找
MethodHandles.Lookup(Class)
。 - 回退到 MethodHandles.lookup().findSpecial(...)
T。 Neidhart 的回答几乎奏效了,但我得到了 java.lang.IllegalAccessException: no private access for invokespecial
改为使用 MethodHandles.privateLookup() 解决了
return MethodHandles.privateLookupIn(clazz,MethodHandles.lookup())
.in(clazz)
.unreflectSpecial(method, clazz)
.bindTo(proxy)
.invokeWithArguments(args);
这是一个完整的示例,其想法是扩展提供的 IMap 的用户可以使用他的自定义界面访问嵌套的嵌套地图
interface IMap {
Object get(String key);
default <T> T getAsAny(String key){
return (T)get(key);
}
default <T extends IMap> T getNestedAs(String key, Class<T> clazz) {
Map<String,Object> nested = getAsAny(key);
return (T)Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, (proxy, method, args) -> {
if (method.getName().equals("get")){
return nested.get(args[0]);
}
return MethodHandles.privateLookupIn(clazz, MethodHandles.lookup())
.in(clazz)
.unreflectSpecial(method, clazz)
.bindTo(proxy)
.invokeWithArguments(args);
}
);
}
}
interface IMyMap extends IMap{
default Integer getAsInt(String key){
return getAsAny(key);
}
default IMyMap getNested(String key){
return getNestedAs(key,IMyMap.class);
}
}
@Test
public void test(){
var data =Map.of("strKey","strValue", "nstKey", Map.of("intKey",42));
IMyMap base = data::get;
IMyMap myMap = base.getNested("nstKey");
System.out.println( myMap.getAsInt("intKey"));
}
In Java 16(来自documentation,里面也有更复杂的例子):
Object proxy = Proxy.newProxyInstance(loader, new Class[] { A.class },
(o, m, params) -> {
if (m.isDefault()) {
// if it's a default method, invoke it
return InvocationHandler.invokeDefault(o, m, params);
}
});
}
Lukas 的回答适用于 Android 8+(早期版本没有默认方法)但依赖于私有 API,后者在后来的 Android 版本中被阻止。幸运的是,替代构造函数也可以工作,目前处于灰名单(不受支持)中。示例(用 Kotlin 编写)可以在这里看到。
@get:RequiresApi(26)
private val newLookup by lazy @TargetApi(26) {
MethodHandles.Lookup::class.java.getDeclaredConstructor(Class::class.java, Int::class.java).apply {
isAccessible = true
}
}
@RequiresApi(26)
fun InvocationHandler.invokeDefault(proxy: Any, method: Method, vararg args: Any?) =
newLookup.newInstance(method.declaringClass, 0xf) // ALL_MODES
.unreflectSpecial(method, method.declaringClass)
.bindTo(proxy)
.invokeWithArguments(*args)