如何获得 Java8方法引用的 MethodInfo?

请看下面的代码:

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

这可以工作,但是方法名称是以字符串的形式传递的,因此即使 myMethod 不存在,它也会编译。

另一方面,Java8引入了一个方法引用特性。它在编译时被检查。有可能使用这个特性来获得方法信息吗?

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);
}
35013 次浏览

Though I haven't tried it myself, I think the answer is "no," since a method reference is semantically the same as a lambda.

No, there is no reliable, supported way to do this. You assign a method reference to an instance of a functional interface, but that instance is cooked up by LambdaMetaFactory, and there is no way to drill into it to find the method you originally bound to.

Lambdas and method references in Java work quite differently than delegates in C#. For some interesting background, read up on invokedynamic.

Other answers and comments here show that it may currently be possible to retrieve the bound method with some additional work, but make sure you understand the caveats.

If you can make the interface Action extend Serializable, then this answer from another question seems to provide a solution (at least on some compilers and runtimes).

In my case I was looking for a way to get rid of this in unit tests:

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

As you can see someone is testing Method getAPoint and checks that the coordinates are as expected, but in the description of each assert was copied and is not in sync with what is checked. Better would be to write this only once.

From the ideas by @ddan I built a proxy solution using 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();
}

No I can simply use

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

and the description of the assert is guaranteed to be in sync with the code.

Downside:

  • Will be slightly slower than above
  • Needs Mockito to work
  • Hardly useful to anything but the usecase above.

However it does show a way how it could be done.

Try this

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

We have published the small library reflection-util that can be used to capture a method name.

Example:

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"

Implementation details: A Proxy subclass of MyClass is created with ByteBuddy and a call to the method is captured to retrieve its name. ClassUtils caches the information such that we do not need to create a new proxy on every invocation.

Please note that this approach is no silver bullet and there are some known cases that don’t work:

  • It doesn’t work for static methods.
  • It doesn’t work if the class is final.
  • We currently do not support all potential method signatures. It should work for methods that do not take an argument such as a getter method.

There may not be a reliable way, but under some circumstances:

  1. your MyClass is not final, and has an accessible constructor (limitation of cglib)
  2. your myMethod is not overloaded, and not static

The you can try using cglib to create a proxy of MyClass, then using an MethodInterceptor to report the Method while the method reference is invoked in a following trial run.

Example code:

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

You will see the following output:

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

While:

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

In the above code, MethodRefWith1Arg is just a syntax sugar for you to reference an non-static method with one arguments. You can create as many as MethodRefWithXArgs for referencing your other methods.

So, I play with this 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();
}
}

output of this code is:

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

And as I notice index of referenced method is always 30. Final code may look like

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

Be careful with this code in production!

You can use my library Reflect Without String

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

You can add safety-mirror to your classpath and do like this:

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

The library also offers a getName(...) method:

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

The library is based on Holger's answer: https://stackoverflow.com/a/21879031/6095334

Edit: The library have various shortcomings which I am slowly becoming aware of. See fx Holger's comment here: How to get the name of the method resulting from a lambda