Як отримати MethodInfo посилання на метод Java 8?


76

Будь ласка, подивіться на такий код:

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

Це працює, але ім'я методу передається як рядок, тому це буде скомпільовано, навіть якщо myMethod не існує.

З іншого боку, Java 8 вводить функцію посилання на метод. Це перевіряється під час компіляції. Можна скористатися цією функцією, щоб отримати інформацію про метод?

printMethodName(MyClass::myMethod);

Повний приклад:

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

Іншими словами, я хотів би мати код, еквівалентний наступному коду C #:

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

Схоже, це можливо, врешті-решт, за допомогою проксі-сервера для запису того, який метод викликається. stackoverflow.com/a/22745127/3478229
ddan

Це можливо, але це обмежує проксі-клас. Наприклад, він не може бути остаточним і повинен мати стандартний загальнодоступний або захищений конструктор. Більше того, це не буде працювати для остаточних методів.
Рафал

Приймаючи застереження (ми не можемо розрізнити лямбди, а лямбда може містити ІФ, які нас вводять в оману), це хороший підхід і корисний в API - ви можете взяти готовий impl github.com/joj-io/joj-reflect/blob / master / src / main / java / io / joj /… за бажанням.
Piotr Findeisen

Я постійно хочу також це робити. "Правильним" способом зробити це в більшості випадків було б створення власної анотації та позначення цього методу за допомогою анотації, але це стає досить громіздким.
Білл К

Можливий дублікат Як отримати об'єкт Method у Java без використання імен рядків методів . Він не згадує посилання на методи, але має кілька розумних відповідей, на відміну від цього питання тут.
Вадзім

Відповіді:


30

Ні, для цього немає надійного, підтримуваного способу. Ви призначаєте посилання на метод екземпляру функціонального інтерфейсу, але цей екземпляр підготовлений LambdaMetaFactory, і немає можливості детально розглянути його, щоб знайти метод, до якого ви спочатку зв’язані.

Лямбди та посилання на методи в Java працюють зовсім інакше, ніж делегати в C #. Деякі цікаві передумови читайте далі invokedynamic.

Інші відповіді та коментарі тут показують, що наразі можливо отримати зв’язаний метод за допомогою додаткової роботи, але переконайтеся, що ви розумієте застереження.


25
Мені цікаво, чи ведеться якась офіційна розмова щодо цього обмеження. Мені здається, що наявність Java-безпечних посилань на методи було б надзвичайно цінним у Java. Це здається ідеальним часом для їх додавання, і було б шкода бачити, що ця можливість (значно) покращити метапрограмування Java втрачена.
Yona Appletree

14
Це не обмеження; просто ви шукаєте іншу функцію, літерали методів . Це звір, який відрізняється від посилань на методи .
Брайан Гетц,

3
Схоже, ваша відповідь неправильна. Це можливо, хоча і не прямолінійно. benjiweber.co.uk/blog/2013/12/28/…
kd8azz

2
@BrianGoetz +100 для літералів методів і при цьому у нього властивостей літералів. Я впевнений, що ваші колеги зі спеціальної команди JPA були б вам вдячні;)
Dexter Meyers

2
@Devabc Звичайно, це може бути можливо в деяких ситуаціях, але немає підтримуваного API, що еквівалентно прикладу C #, наданому OP. Я думаю, що це є розумною основою для моєї відповіді, і я дотримуюсь її. Інші можливості, такі як використання проксі-серверів або навіть аналіз аргументів методу завантаження в байт-коді, є ненадійними або накладають додаткові обмеження. Чи варто їх згадувати в окремій відповіді? Звичайно. Вони роблять мою відповідь неправильною або гідною відхилення? Не розумію як. Тим не менш, я переформулював свою відповідь, щоб звучати менш абсолютно.
Mike Strobel

8

У моєму випадку я шукав спосіб позбутися цього в модульних тестах:

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

Як бачите, хтось тестує Метод getAPointі перевіряє, чи відповідають координати належним чином, але в описі кожне твердження було скопійовано і не синхронізується з тим, що було перевірено. Краще було б написати це лише один раз.

З ідей @ddan я створив проксі-рішення за допомогою Mockito:

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

Ні, я можу просто використовувати

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

а опис твердження гарантовано синхронізується з кодом.

Недолік:

  • Буде трохи повільніше, ніж вище
  • Потрібно Mockito працювати
  • Навряд чи корисний для чого-небудь, крім уживаного вище.

Однак це показує спосіб, як це можна зробити.


Дякую, BTW, замість Mockito, ми можемо зобов'язати розробників розширювати самі класи bean (і впроваджувати String getMethod()в їхніх макетах)
басейн


4

Ви можете додати дзеркало безпеки до своєї класної доріжки і зробити так:

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

Бібліотека також пропонує getName(...)метод:

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

Бібліотека базується на відповіді Холгера: https://stackoverflow.com/a/21879031/6095334

Редагувати: Бібліотека має різні недоліки, про які я повільно усвідомлюю. Див. Коментар fx Holger тут: Як отримати назву методу, отриманого від лямбда-сигналу


3

Якщо ви можете Actionрозширити інтерфейс Serializable, тоді ця відповідь на інше питання, мабуть, пропонує рішення (принаймні на деяких компіляторах та робочих середовищах).


3

Можливо, не існує надійного способу, але за певних обставин:

  1. your MyClassне є остаточним і має доступний конструктор (обмеження cglib)
  2. ваш myMethodне перевантажений і не статичний

Ви можете спробувати використати cglib для створення проксі-сервера MyClass, а потім використовувати a MethodInterceptorдля повідомлення про Methodчас, коли посилання на метод викликається в наступному пробному запуску.

Приклад коду:

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

Ви побачите такий результат:

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

Поки:

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

У наведеному вище коді - MethodRefWith1Argце просто синтаксичний цукор для посилання на нестатичний метод з одним аргументом. Ви можете створити стільки, скільки MethodRefWithXArgsдля посилання на інші ваші методи.


2

Ми опублікували невелику бібліотеку відображення-util, яка може бути використана для захоплення імені методу.

Приклад:

class MyClass {

    public void myMethod() {
    }

}

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

Деталі реалізації: Підклас проксі MyClassстворюється за допомогою ByteBuddy, і виклик методу фіксується для отримання його імені. ClassUtilsкешує інформацію так, що нам не потрібно створювати новий проксі-сервер при кожному виклику.

Зверніть увагу, що такий підхід не працює зі статичними методами.


Дуже обмежене, не загальне рішення
xiaohuo,

А також це залежить від небезпечних і не може бути використаний на сучасних JDK
rumatoest

1
@rumatoest: Reflection-Util підтримує сучасні JDK. Не могли б ви подати проблему на GitHub для випадку використання, який для вас не працює?
Benedikt Waldvogel

-1

Отже, я граю з цим кодом

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

вихід цього коду:

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

І як я помічаю, індекс посиланого методу завжди дорівнює 30. Остаточний код може виглядати так

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

Будьте обережні з цим кодом у виробництві!


О ... так, це ризиковано для виробництва ;-)
Рафал

-1

Ви можете використовувати мою бібліотеку Reflect Without String

Method myMethod = ReflectWithoutString.methodGetter(MyClass.class).getMethod(MyClass::myMethod);

1
Я перевірив. Це не працює ні для фінальних методів, ні для заключних класів, ні для статичних методів.
Рафал

1
@Rafal Так, це так. Я писав у документі. Але Ваш голос проти не має сенсу. По-перше, у вашому питанні немає цього обмеження. Я збираюся дати вам рішення лише для вашого запитання. По-друге, я перевірив 4 відповіді вище, хто дає рішення та має більше 1 голосу за. 3 з них не можуть працювати для остаточних / статичних методів. По-третє, рідко коли відображається статичний метод, навіщо це потрібно?
Дін Сю

Я можу змінити голос проти, але ви повинні відредагувати свою відповідь, щоб дозволити мені це зробити (через обмеження Stackoveflow). Голоси за інші запитання зробив не я. Моє запитання не має обмежень ... так ... тому я очікую відповіді, яка охоплює всі випадки або, принаймні, ясно, які не розглядаються.
Рафал

@Rafal Дякую, сер. Це не має значення. Якщо ви бачите мене десь ще, будь ласка, не голосуйте необережно :)
Дін Сю

1
Тут відсутня найважливіша інформація, тобто як працює ваша бібліотека. Якщо Github піде вниз, тоді ця відповідь марна. Інші відповіді принаймні посилаються на якийсь відповідний код на StackOverflow.
jmiserez

-3

Спробуйте це

Thread.currentThread().getStackTrace()[2].getMethodName();

3
Будь ласка, додайте до своєї відповіді кілька слів із поясненнями. Крім того, OP не бажає імені методу, який зараз викликається, а методу, посилання на який пересилається.
mkl
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.