Wie erhalte ich die MethodInfo einer Java 8-Methodenreferenz?

76

Bitte schauen Sie sich den folgenden Code an:

Method methodInfo = MyClass.class.getMethod("myMethod");

Dies funktioniert, aber der Methodenname wird als Zeichenfolge übergeben, sodass dies auch dann kompiliert wird, wenn myMethod nicht vorhanden ist.

Auf der anderen Seite führt Java 8 eine Methodenreferenzfunktion ein. Es wird zur Kompilierungszeit überprüft. Ist es möglich, diese Funktion zu verwenden, um Methodeninformationen abzurufen?

printMethodName(MyClass::myMethod);

Vollständiges Beispiel:

@FunctionalInterface
private interface Action {

    void invoke();
}

private static class MyClass {

    public static void myMethod() {
    }
}

private static void printMethodName(Action action) {
}

public static void main(String[] args) throws NoSuchMethodException {
    // This works, but method name is passed as a string, so this will compile
    // even if myMethod does not exist
    Method methodInfo = MyClass.class.getMethod("myMethod");

    // Here we pass reference to a method. It is somehow possible to
    // obtain java.lang.reflect.Method for myMethod inside printMethodName?
    printMethodName(MyClass::myMethod);
}

Mit anderen Worten, ich hätte gerne einen Code, der dem folgenden C # -Code entspricht:

    private static class InnerClass
    {
        public static void MyMethod()
        {
            Console.WriteLine("Hello");
        }
    }

    static void PrintMethodName(Action action)
    {
        // Can I get java.lang.reflect.Method in the same way?
        MethodInfo methodInfo = action.GetMethodInfo();
    }

    static void Main()
    {
        PrintMethodName(InnerClass.MyMethod);
    }
Rafal
quelle
Es sieht so aus, als wäre es doch möglich, mit einem Proxy aufzuzeichnen, welche Methode aufgerufen wird. stackoverflow.com/a/22745127/3478229
ddan
Es ist möglich, aber dies schränkt die Proxy-Klasse ein. Beispielsweise kann es nicht endgültig sein und muss über einen öffentlichen oder geschützten Standardkonstruktor verfügen. Darüber hinaus funktioniert dies nicht für endgültige Methoden.
Rafal
Das Akzeptieren von Vorbehalten (wir können Lambdas nicht unterscheiden, und ein Lambda kann IFs enthalten, die uns täuschen) ist ein guter Ansatz und nützlich in APIs - Sie können sich ein fertiges impl github.com/joj-io/joj-reflect/blob schnappen / master / src / main / java / io / joj /… wenn du willst.
Piotr Findeisen
Ich möchte das auch weiterhin tun. Die "richtige" Methode, dies zu tun, besteht in den meisten Fällen darin, eine benutzerdefinierte Anmerkung zu erstellen und diese Methode über die Anmerkung zu kennzeichnen. Dies wird jedoch ziemlich schnell umständlich.
Bill K
Mögliches Duplikat von Wie man ein Methodenobjekt in Java erhält, ohne Methodenzeichenfolgennamen zu verwenden . Im Gegensatz zu dieser Frage hier wird die Methodenreferenz nicht erwähnt, es gibt jedoch mehrere vernünftige Antworten.
Vadzim

Antworten:

30

Nein, es gibt keine zuverlässige und unterstützte Möglichkeit, dies zu tun. Sie weisen einer Instanz einer Funktionsschnittstelle einen Methodenverweis zu, der jedoch von dieser Instanz zusammengestellt wird LambdaMetaFactory, und es gibt keine Möglichkeit, einen Drill-Drill durchzuführen, um die Methode zu finden, an die Sie ursprünglich gebunden waren.

Lambdas und Methodenreferenzen in Java funktionieren ganz anders als Delegaten in C #. Weitere interessante Hintergrundinformationen finden Sie unter invokedynamic.

Andere Antworten und Kommentare hier zeigen, dass es derzeit möglicherweise möglich ist, die gebundene Methode mit einigen zusätzlichen Arbeiten abzurufen, aber stellen Sie sicher, dass Sie die Einschränkungen verstehen.

Mike Strobel
quelle
25
Ich bin gespannt, ob es ein offizielles Gespräch über diese Einschränkung gibt. Es scheint mir, dass typsichere Methodenreferenzen in Java unglaublich wertvoll wären. Dies scheint der perfekte Zeitpunkt zu sein, um sie hinzuzufügen, und es wäre eine Schande zu sehen, dass diese Gelegenheit zur (signifikanten) Verbesserung der Java-Metaprogrammierung verloren geht.
Yona Appletree
14
Dies ist keine Einschränkung. Es ist einfach so, dass Sie nach einer anderen Funktion suchen, nach Methodenliteralen . Dies ist ein anderes Tier als Methodenreferenzen .
Brian Goetz
3
Es sieht so aus, als ob Ihre Antwort falsch ist. Es ist möglich, wenn auch nicht einfach. benjiweber.co.uk/blog/2013/12/28/…
kd8azz
2
@BrianGoetz +100 für Methodenliterale und dabei Eigenschaftsliterale. Ich bin sicher, Ihre Kollegen im JPA-Spezifikationsteam wären intern dankbar;)
Dexter Meyer
2
@Devabc Natürlich kann es sein , möglich , in einigen Situationen, aber es gibt keine API unterstützt, die mit dem C # Beispiel durch das OP gleichwertig ist. Ich denke, das ist eine vernünftige Grundlage für meine Antwort, und ich stehe dazu. Andere Möglichkeiten, wie die Verwendung von Proxys oder sogar die Analyse der Argumente der Bootstrap-Methode im Bytecode, sind entweder unzuverlässig oder erfordern zusätzliche Einschränkungen. Sind sie in einer separaten Antwort erwähnenswert? Sicher. Machen sie meine Antwort falsch oder einer Ablehnung würdig? Ich sehe nicht wie. Trotzdem habe ich meine Antwort umformuliert, um weniger absolut zu klingen.
Mike Strobel
8

In meinem Fall suchte ich nach einer Möglichkeit, dies in Unit-Tests zu beseitigen:

Point p = getAPoint();
assertEquals(p.getX(), 4, "x");
assertEquals(p.getY(), 6, "x");

Wie Sie sehen können, testet jemand die Methode getAPointund überprüft, ob die Koordinaten wie erwartet sind, aber in der Beschreibung jeder Behauptung wurde kopiert und ist nicht synchron mit dem, was überprüft wird. Besser wäre es, dies nur einmal zu schreiben.

Aus den Ideen von @ddan habe ich mit Mockito eine Proxy-Lösung erstellt:

private<T> void assertPropertyEqual(final T object, final Function<T, ?> getter, final Object expected) {
    final String methodName = getMethodName(object.getClass(), getter);
    assertEquals(getter.apply(object), expected, methodName);
}

@SuppressWarnings("unchecked")
private<T> String getMethodName(final Class<?> clazz, final Function<T, ?> getter) {
    final Method[] method = new Method[1];
    getter.apply((T)Mockito.mock(clazz, Mockito.withSettings().invocationListeners(methodInvocationReport -> {
        method[0] = ((InvocationOnMock) methodInvocationReport.getInvocation()).getMethod();
    })));
    return method[0].getName();
}

Nein, ich kann einfach verwenden

assertPropertyEqual(p, Point::getX, 4);
assertPropertyEqual(p, Point::getY, 6);

und die Beschreibung der Zusicherung ist garantiert mit dem Code synchron.

Nachteil:

  • Wird etwas langsamer als oben sein
  • Muss Mockito arbeiten
  • Kaum nützlich für irgendetwas anderes als den obigen Anwendungsfall.

Es zeigt jedoch einen Weg, wie es gemacht werden könnte.

Yankee
quelle
Übrigens, anstelle von Mockito können wir die Entwickler verpflichten, die Bean-Klassen selbst zu erweitern (und String getMethod()in ihren Mocks zu implementieren )
Becken
3

Wenn Sie die Schnittstelle Actionerweitern können Serializable, scheint diese Antwort auf eine andere Frage eine Lösung zu bieten (zumindest bei einigen Compilern und Laufzeiten).

user102008
quelle
3

Es gibt möglicherweise keinen zuverlässigen Weg, aber unter bestimmten Umständen:

  1. Ihr MyClassist nicht endgültig und hat einen zugänglichen Konstruktor (Einschränkung von cglib)
  2. Ihr myMethodist nicht überlastet und nicht statisch

Sie können versuchen, mit cglib einen Proxy von zu erstellen MyClassund dann mit a MethodInterceptorzu melden, Methodwährend die Methodenreferenz in einem folgenden Testlauf aufgerufen wird.

Beispielcode:

public static void main(String[] args) {
    Method m = MethodReferenceUtils.getReferencedMethod(ArrayList.class, ArrayList::contains);
    System.out.println(m);
}

Sie sehen die folgende Ausgabe:

public boolean java.util.ArrayList.contains(java.lang.Object)

Während:

public class MethodReferenceUtils {

    @FunctionalInterface
    public static interface MethodRefWith1Arg<T, A1> {
        void call(T t, A1 a1);
    }

    public static <T, A1> Method getReferencedMethod(Class<T> clazz, MethodRefWith1Arg<T, A1> methodRef) {
        return findReferencedMethod(clazz, t -> methodRef.call(t, null));
    }

    @SuppressWarnings("unchecked")
    private static <T> Method findReferencedMethod(Class<T> clazz, Consumer<T> invoker) {
        AtomicReference<Method> ref = new AtomicReference<>();
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                ref.set(method);
                return null;
            }
        });
        try {
            invoker.accept((T) enhancer.create());
        } catch (ClassCastException e) {
            throw new IllegalArgumentException(String.format("Invalid method reference on class [%s]", clazz));
        }

        Method method = ref.get();
        if (method == null) {
            throw new IllegalArgumentException(String.format("Invalid method reference on class [%s]", clazz));
        }

        return method;
    }
}

Im obigen Code MethodRefWith1Argist dies nur ein Syntaxzucker, mit dem Sie auf eine nicht statische Methode mit einem Argument verweisen können. Sie können so viele erstellen, wie Sie MethodRefWithXArgsauf Ihre anderen Methoden verweisen.

vivimice
quelle
3

Sie können Ihrem Klassenpfad einen Sicherheitsspiegel hinzufügen und dies folgendermaßen tun:

Method m1 = Types.createMethod(Thread::isAlive)  // Get final method
Method m2 = Types.createMethod(String::isEmpty); // Get method from final class
Method m3 = Types.createMethod(BufferedReader::readLine); // Get method that throws checked exception
Method m4 = Types.<String, Class[]>createMethod(getClass()::getDeclaredMethod); //to get vararg method you must specify parameters in generics
Method m5 = Types.<String>createMethod(Class::forName); // to get overloaded method you must specify parameters in generics
Method m6 = Types.createMethod(this::toString); //Works with inherited methods

Die Bibliothek bietet auch eine getName(...)Methode:

assertEquals("isEmpty", Types.getName(String::isEmpty));

Die Bibliothek basiert auf Holgers Antwort: https://stackoverflow.com/a/21879031/6095334

Bearbeiten: Die Bibliothek hat verschiedene Mängel, die mir langsam bewusst werden. Siehe fx Holgers Kommentar hier: So erhalten Sie den Namen der Methode, die aus einem Lambda resultiert

Hervian
quelle
1

Wir haben das Reflection-Util für kleine Bibliotheken veröffentlicht, mit dem ein Methodenname erfasst werden kann.

Beispiel:

class MyClass {

    public void myMethod() {
    }

}

String methodName = ClassUtils.getVoidMethodName(MyClass.class, MyClass::myMethod);
System.out.println(methodName); // prints "myMethod"

Implementierungsdetails: MyClassMit ByteBuddy wird eine Proxy-Unterklasse von erstellt und ein Aufruf der Methode erfasst, um ihren Namen abzurufen. ClassUtilsspeichert die Informationen so zwischen, dass wir nicht bei jedem Aufruf einen neuen Proxy erstellen müssen.

Bitte beachten Sie, dass dieser Ansatz mit statischen Methoden nicht funktioniert .

Benedikt Waldvogel
quelle
Ist sehr begrenzt, keine allgemeine Lösung
Xiaohuo
Und es hängt auch von Unsafe ab und kann nicht auf modernen JDKs verwendet werden
rumatoest
@rumatoest: Reflection-Util unterstützt moderne JDKs. Könnten Sie auf GitHub ein Problem für den Anwendungsfall einreichen, der für Sie nicht funktioniert?
Benedikt Waldvogel
-1

Also spiele ich mit diesem Code

import sun.reflect.ConstantPool;

import java.lang.reflect.Method;
import java.util.function.Consumer;

public class Main {
    private Consumer<String> consumer;

    Main() {
        consumer = this::test;
    }

    public void test(String val) {
        System.out.println("val = " + val);
    }

    public void run() throws Exception {
        ConstantPool oa = sun.misc.SharedSecrets.getJavaLangAccess().getConstantPool(consumer.getClass());
        for (int i = 0; i < oa.getSize(); i++) {
            try {
                Object v = oa.getMethodAt(i);
                if (v instanceof Method) {
                    System.out.println("index = " + i + ", method = " + v);
                }
            } catch (Exception e) {
            }
        }
    }

    public static void main(String[] args) throws Exception {
        new Main().run();
    }
}

Ausgabe dieses Codes ist:

index = 30, method = public void Main.test(java.lang.String)

Und wie ich bemerke, ist der Index der referenzierten Methode immer 30. Der endgültige Code kann so aussehen

public Method unreference(Object methodRef) {
        ConstantPool constantPool = sun.misc.SharedSecrets.getJavaLangAccess().getConstantPool(methodRef.getClass());
        try {
            Object method = constantPool.getMethodAt(30);
            if (method instanceof Method) {
                return (Method) method;
            }
        }catch (Exception ignored) {
        }
        throw new IllegalArgumentException("Not a method reference.");
    }

Seien Sie vorsichtig mit diesem Code in der Produktion!

Dmitriy V.
quelle
Oh ... ja, es ist zu riskant für die Produktion ;-)
Rafal
-1

Du kannst meine Bibliothek benutzen Reflect Without String

Method myMethod = ReflectWithoutString.methodGetter(MyClass.class).getMethod(MyClass::myMethod);
Dean Xu
quelle
1
Ich habe nachgesehen. Es funktioniert weder für endgültige Methoden noch für endgültige Klassen oder statische Methoden.
Rafal
1
@ Rafal Ja, das ist es. Ich hatte im Dokument geschrieben. Aber Ihre Ablehnung macht keinen Sinn. Erstens hat Ihre Frage keine Einschränkung. Ich werde hier nur eine Lösung für Ihre Frage geben. Zweitens habe ich oben 4 Antworten überprüft, die eine Lösung geben und mehr als 1 Gegenstimme haben. 3 von ihnen können nicht für endgültige / statische Methoden arbeiten. Drittens ist es selten, eine statische Methode zu reflektieren. Warum brauchen Sie das?
Dean Xu
Ich kann die Downvote ändern, aber Sie müssen Ihre Antwort bearbeiten, damit ich das tun kann (aufgrund der Stackoveflow-Einschränkung). Upvotes für andere Fragen wurden nicht von mir gemacht. Meine Frage hat keine Einschränkungen ... ja ... also erwarte ich eine Antwort, die alle Fälle abdeckt oder zumindest klar ist, welche nicht abgedeckt sind.
Rafal
@ Rafal Danke, Sir. Es spielt keine Rolle. Wenn Sie mich woanders sehen, stimmen Sie bitte nicht nachlässig ab :)
Dean Xu
1
Die entscheidende Information fehlt hier, dh wie Ihre Bibliothek funktioniert. Wenn Github untergeht, ist diese Antwort nutzlos. Die anderen Antworten verweisen zumindest auf einen relevanten Code in StackOverflow.
jmiserez
-3

Versuche dies

Thread.currentThread().getStackTrace()[2].getMethodName();
Asghar Nemati
quelle
3
Bitte fügen Sie Ihrer Antwort einige erklärende Worte hinzu. Darüber hinaus möchte das OP nicht den Namen einer aktuell aufgerufenen Methode, sondern einer Methode, auf die ein Verweis weitergeleitet wird.
mkl