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

2022-09-01 07:45:07

由于Java 8接口可以具有默认方法。我知道如何从实现方法中显式调用该方法,即(请参阅在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();
  }
}

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

  • 该问题中描述的问题旨在如何通过反射来调用它 - 因此没有区分默认和覆盖的方法 - 这很简单,您只需要一个实例。
  • 其中一个答案 - 使用方法句柄 - 仅适用于令人讨厌的hack(恕我直言),例如将访问修饰符更改为查找类的字段,这是“解决方案”的同一类别,如下所示:使用Java反射更改私有静态最终字段很高兴知道它是可能的,但我不会在生产中使用它 - 我正在寻找一种“官方”方法来做到这一点。

被扔进去IllegalAccessExceptionunreflectSpecial

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)

答案 1

在JDK 8 - 10中使用时,我也遇到了类似的问题,它们的行为不同。我已经在这里详细地写了关于正确解决方案的博客MethodHandle.Lookup

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

这是同时适用于私有可访问和私有无法访问接口的唯一方法。但是,上述方法确实非法地对 JDK 内部进行反射访问,这些访问在将来的 JDK 版本中将不再有效,或者在 JVM 上指定。--illegal-access=deny

此方法适用于 Java 9 和 10,但不适用于 Java 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上运行,就可以了。直到你不:)


答案 2

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

Example target = new Example();
...

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

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

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

MethodHandles/Lookup 类的当前实现将不允许对任何无法从当前调用方类进行私有访问的类调用 invokeSpecial。有各种可用的解决方法,但所有这些都需要使用反射来使构造函数/方法可访问,如果安装了安全管理器,这可能会失败。