簡體   English   中英

如何獲取 Java 8 方法引用的 MethodInfo?

[英]How to get the MethodInfo of a Java 8 method reference?

請看下面的代碼:

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

不,沒有可靠的、受支持的方法來做到這一點。 您將方法引用分配給函數式接口的實例,但該實例是由LambdaMetaFactory ,並且無法深入研究它以找到您最初綁定到的方法。

Java 中的 Lambda 和方法引用的工作方式與 C# 中的委托截然不同。 對於一些有趣的背景,請閱讀invokedynamic

此處的其他答案和評論表明,目前可以通過一些額外的工作來檢索綁定方法,但請確保您了解注意事項。

就我而言,我正在尋找一種在單元測試中擺脫這種情況的方法:

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 才能工作
  • 除了上面的用例之外,幾乎沒有任何用處。

然而,它確實展示了如何做到這一點。

雖然我自己沒有嘗試過,但我認為答案是“不”,因為方法引用在語義上與 lambda 相同。

您可以將安全鏡像添加到您的類路徑並執行以下操作:

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

該庫基於 Holger 的回答: https : //stackoverflow.com/a/21879031/6095334

編輯:圖書館有我慢慢意識到的各種缺點。 在此處查看 fx Holger 的評論: How to get the name of the method results from a lambda

如果您可以使接口Action擴展Serializable ,那么來自另一個問題的這個答案似乎提供了一個解決方案(至少在某些編譯器和運行時上)。

可能沒有可靠的方法,但在某些情況下:

  1. 你的MyClass不是最終的,並且有一個可訪問的構造函數(cglib 的限制)
  2. 你的myMethod沒有重載,也不是靜態的

您可以嘗試使用 cglib 創建MyClass的代理,然后在隨后的試運行中調用方法引用時使用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來引用您的其他方法。

我們發布了可用於捕獲方法名稱的小型庫reflection-util

例子:

class MyClass {

    private int value;

    public void myMethod() {
    }

    public int getValue() {
        return value;
    }

}

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

String getterName = ClassUtils.getMethodName(MyClass.class, MyClass::getValue);
System.out.println(getterName); // prints "getValue"

實現細節:使用ByteBuddy創建MyClass的 Proxy 子類,並捕獲對該方法的調用以檢索其名稱。 ClassUtils緩存信息,這樣我們就不需要在每次調用時都創建一個新的代理。

請注意,這種方法不是靈丹妙葯,有一些已知的情況是行不通的:

  • 它不適用於靜態方法。
  • 如果類是final則不起作用。
  • 我們目前不支持所有潛在的方法簽名。 它應該適用於不帶參數的方法,例如 getter 方法。

您可以使用我的庫Reflect Without String

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

所以,我玩這個代碼

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

在生產中小心這段代碼!

嘗試這個

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM