【问题标题】:How to explicitly invoke default method from a dynamic Proxy?如何从动态代理显式调用默认方法?
【发布时间】:2016-10-15 05:00:12
【问题描述】:

因为 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 (imho),例如将访问修饰符更改为查找类的字段,这是同一类别的“解决方案”,如下所示:Change private static final field using Java reflection很高兴知道这是可能的,但我不会在生产中使用它 - 我正在寻找一种“官方”的方式来做到这一点。

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$0(Example.java:30)
at example.Example$$Lambda$1/1342443276.invoke(Unknown Source)

【问题讨论】:

  • "我正在寻找一种“官方”的方式来做到这一点" 我可能弄错了,但我担心官方你不应该能够调用方法如果您的子类型覆盖了它,则来自超类型。假设您的超类型有acceptSquare 方法可以接受任何正方形,但是您的子类型专门只处理红色正方形,因此它相应地覆盖它以添加颜色测试(之后它调用@987654330 @)。因此,允许某人从此类方法的超类型(甚至通过反射)从外部实现调用可能是一个很大的安全漏洞。
  • mixins 怎么样 - 使用动态代理向现有类添加功能?即我有一个实例,并希望通过在运行时向实例“添加”具有默认方法的接口来添加其他功能。一定有办法
  • 我相应地更新了示例,一开始希望它尽可能简单,但希望我的意图现在变得更清晰
  • default 方法的调用目标必须是该interface 的实例。在您的示例代码中,它是一个Object——它应该如何工作?

标签: java reflection java-8 default-method


【解决方案1】:

用途:

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

【讨论】:

  • 我试过了,给了我Caused by: java.lang.IllegalAccessException: no private access for invokespecial: interface example.IExample, from example.IExample/package
【解决方案2】:

如果您使用具体的 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。有各种可用的变通方法,但它们都需要使用反射来使构造函数/方法可访问,如果安装了 SecurityManager,这可能会失败。

【讨论】:

  • hm,这也适用于匿名类......但鉴于,我之前不知道接口(即因为它是一个参数),我可以动态创建匿名类的实例吗? (代理似乎不起作用,字节码生成也不是一个选项)
  • 本例中的ex 是什么?这里必须是Example 的实例,那么为什么不直接使用target 实例呢?除此之外,它与直接使用ExampleMixin 具有相同的限制;只有当指定的类与周围的代码(调用lookup()的代码)有内部类关系时,它才有效。
  • 这是一个错误,ex确实应该被target替换。
  • @Gerald Mücke:无论您使用哪种方法,尝试生成实现都是没有意义的。当您调用MethodHandles.lookup() 时,您将获得一个允许对您自己进行私有访问的上下文。一旦您使用与您自己没有内部类关系的类(包括各种生成的类)调用in(someOtherClass),您将失去unreflectSpecial 所需的私有访问属性。
  • @Holger:我猜你在检查了 MethodHandles 的 Java 8 源代码之后是对的。那么就没有不涉及一些反射黑客的解决方法了吗?
【解决方案3】:

如果你只有一个接口,而你只能访问一个类对象是一个扩展你的基接口的接口,并且你想在没有实现该接口的类的真实实例的情况下调用默认方法,你可以:

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

【讨论】:

  • MethodHandles.Lookup 给出“非法反射访问”,与上面报道的相同。
【解决方案4】:

在 JDK 8 - 10 中使用 MethodHandle.Lookup 时,我也遇到过类似问题,它们的行为不同。 I've blogged about the correct solution here in detail.

这种方法适用于 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<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();
    }
}

这是唯一适用于私有可访问和私有不可访问接口的方法。但是,上述方法对 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 上运行,就可以了。直到你不是:)

【讨论】:

  • 不幸的是,这两种方法都不适用于 Android,因为 Android 会阻止对 Java 8 方法的反射访问。 :(
  • 对于 Android 8+,您需要调用构造函数并将整数参数设置为 (ALL_MODES),因为如果您的目标 API 28+,则其他 API 已列入灰名单且无法使用。跨度>
  • @Mygod:这主要是一个 JDK 问答。随意提供Android特定的答案,我相信它对其他人有用......
  • 需要明确的是,如果 JVM 以 --illegal-access=deny 启动(默认情况下不是这样),Java 8 概述的方法只会在 Java 9、10、11 上失败。否则,此解决方案将在控制台上打印一条警告消息,但它仍然可以工作。
  • 您似乎错过了实际问题的上下文是 Java 平台模块系统 (JPMS)。在这种情况下,您建议的解决方案都不起作用,因为您总是收到 IllegalAccessException。但是,如果您可以从模块中看到接口,您应该能够调用默认方法(尤其是动态代理),否则 Java 的基本概念是有缺陷的。似乎用户需要升级到 Java 16/17 并按照@John 的建议使用 InvocationHandler.invokeDefault
【解决方案5】:

我们可以看看spring如何处理默认方法。

  1. 首先尝试调用公共方法MethodHandles.privateLookupIn(Class,Lookup)。这应该在 jdk9+ 上成功。
  2. 尝试使用包私有构造函数MethodHandles.Lookup(Class) 创建一个查找。
  3. 回退到 MethodHandles.lookup().findSpecial(...)

https://github.com/spring-projects/spring-data-commons/blob/2.1.8.RELEASE/src/main/java/org/springframework/data/projection/DefaultMethodInvokingMethodInterceptor.java

【讨论】:

  • spring 根本不支持 Java 模块。因此,当使用弹簧“解决方案”时,错误IllegalAccessException: no private access for invokespecial: interface example.ExampleMixin, from example.ExampleMixin/package 不会消失。
【解决方案6】:

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

【讨论】:

  • 似乎需要 Java 9。
【解决方案7】:

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

【讨论】:

【解决方案8】:

Lukas 的答案适用于 Android 8+(早期版本没有默认方法),但依赖于在后来的 Android 版本中被阻止的私有 API。幸运的是,替代构造函数也可以工作,并且目前处于灰名单(不支持)中。示例(用 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)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-01-26
    • 2016-04-04
    • 2021-10-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多