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

时间:2016-06-14 12:42:54

标签: java reflection java-8 default-method

由于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中提出了类似的问题,但由于两个原因,这并没有解决我的问题:

  • 该问题中描述的问题针对如何通过反射一般调用它 - 所以没有区分default和overriden方法 - 这很简单,你只需要一个实例。 / LI>
  • 其中一个答案 - 使用方法句柄 - 只能使用讨厌的黑客(imho),例如将访问修饰符更改为查找类的字段,这是&#34;解决方案&#34;像这样:Change private static final field using Java reflection很高兴知道它可能,但我不会在生产中使用它 - 我正在寻找和#34;官员&#34;这样做的方法。

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)

7 个答案:

答案 0 :(得分:7)

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

Example target = new Example();
...

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

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

编辑:此解决方案仅在相关课程(上述代码中的示例)可通过来电者代码进行私人访问时才有效,例如:一个匿名的内部阶级。

MethodHandles / Lookup类的当前实现不允许在任何非当前调用者类可以访问的类上调用invokeSpecial。有各种可用的解决方法,但所有这些都需要使用反射来使构造函数/方法可访问,如果安装了SecurityManager,这可能会失败。

答案 1 :(得分:6)

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

        duck.quack();
    }
}

解决方案

只需实现上述两种解决方案,并检查您的代码是在JDK 8上运行还是在以后的JDK上运行,您就可以了。直到你不是:)

答案 2 :(得分:3)

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

答案 3 :(得分:2)

如果您拥有的只是一个接口,并且您有权访问的是一个类对象,那么扩展您的基本接口的接口,并且您希望在没有实现该接口的类的实际实例的情况下调用默认方法,你可以:

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

答案 4 :(得分:1)

使用:

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

答案 5 :(得分:0)

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

  1. 首先尝试调用公共方法MethodHandles.privateLookupIn(Class,Lookup)。这应该在jdk9 +上成功完成。
  2. 尝试使用包私有构造函数MethodHandles.Lookup(Class)创建一个Lookup。
  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

答案 6 :(得分:0)

T。 Neidhart的回答几乎奏效,但是我得到了 java.lang.IllegalAccessException:invokeprivial的私有访问

更改使用 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"));
}