web-dev-qa-db-ja.com

動的プロキシからデフォルトのメソッドを明示的に呼び出す方法は?

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つのデフォルトメソッドrefletively 、しかしこれは2つの理由で私の問題を解決していません:

  • その質問で説明されている問題は、リフレクションを介して呼び出す方法を目的としています(= /// =)一般的に-したがって、デフォルトのメソッドとオーバーライドされたメソッドの区別はありません-これは単純で、インスタンスのみが必要です。
  • 答えの1つ(メソッドハンドルの使用)は、ルックアップクラスのフィールドへのアクセス修飾子の変更などの厄介なハック(imho)でのみ機能します。これは、次のような「ソリューション」の同じカテゴリです。 プライベート静的最終フィールドの変更using Java Reflection可能であることを知っておくのは良いことですが、本番環境では使用しません-探していますそれを行うための「公式の」方法。

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)
28
Gerald Mücke

具象の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がインストールされている場合はおそらく失敗します。

8
T. Neidhart

JDK8-10でMethodHandle.Lookupを使用すると、動作が異なる同様の問題にも悩まされてきました。 ここで正しい解決策について詳しくブログに書いています

このアプローチはJava 8で機能します

Java 8では、理想的なアプローチは、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();
                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で実行されているかどうかを確認するだけで、問題はありません。あなたがいないまで:)

8
Lukas Eder

持っているのがインターフェースだけで、アクセスできるのがクラスオブジェクトだけで、基本インターフェースを拡張するインターフェースであり、インターフェースを実装するクラスの実際のインスタンスなしでデフォルトのメソッドを呼び出したい場合は、次のことができます。

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

使用する:

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

スプリングプロセスのデフォルトメソッドがどのようになっているのかがわかります。

  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

0
martian