Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Obtain java.lang.reflect.Executable of current or calling method?

How can one obtain objects of the following types from specific instances of their represented language feature in the code:

  • java.lang.reflect.Executable
  • java.lang.reflect.Parameter

Ideally the providing method would be a caller of the obtaining method or the obtaining method itself.

Note that I have found two kinds of answers on SO:

  • how to get a Parameter from a given Executable object (trivial!)
  • how to get the name (not signature!) of a method from the call stack

Neither helps to obtain the Parameter to a particular method whose name is not unique.

Consider in particular how this answer and this one to similar questions gloss over the selection of the desired method.

Now, there exists Class.getMethod(String, Class<?>... types) but it does not seem that one could generate the "types" parameter automatically from an existing method definition?

Use case:

public class MyAssertions
{
  private static final String DEFAULT_MESSAGE_NOT_NULL = "Parameter must not be null: ";

  /* Ideal. */
  public static void notNull(Object ref, Parameter param)
  {
    if(ref == null)
      throw new IllegalArgumentException(DEFAULT_MESSAGE_NOT_NULL + param.getName());
  }

  /* Still okay. */
  public static void notNull(Object ref, Executable caller, int param)
  {
    if(ref == null)
      throw new IllegalArgumentException(DEFAULT_MESSAGE_NOT_NULL
                                       + caller.getParameters()[param].getName());
  }

  /* Hell no! */
  public static void notNull(Object ref, Class<?> caller, String method, Object[] paramTypes, int param)
  {
    if(ref == null)
      throw new IllegalArgumentException(DEFAULT_MESSAGE_NOT_NULL
                                       + caller.getMethod(method, paramTypes)
                                         .getParameters()[param].getName());
  }
}
like image 899
Zsar Avatar asked Jun 15 '17 09:06

Zsar


1 Answers

You can use bytecode utilities to get the bytecode infomation. And then use the line number info in StackTraceElement to get the method.

Here I use javassist.

  private static Optional<Method> get(StackTraceElement e) throws NotFoundException, ClassNotFoundException {
    Class<?> clz = Class.forName(e.getClassName());
    int line = e.getLineNumber();
    ClassPool pool = ClassPool.getDefault();
    CtClass cc = pool.get(clz.getName());
    return Observable.fromArray(cc.getDeclaredMethods())
        .sorted(Comparator.comparing(m -> m.getMethodInfo().getLineNumber(0)))
        .filter(m -> m.getMethodInfo().getLineNumber(0) <= line)
        .map(Optional::of)
        .blockingLast(Optional.empty())
        .map(m -> uncheck(() -> clz.getDeclaredMethod(m.getName(),
            Arrays.stream(m.getParameterTypes()).map(c -> uncheck(() -> nameToClass(c.getName()))).toArray(Class[]::new))));
  }

  private static Class<?> nameToClass(String name) throws ClassNotFoundException {
    switch (name) {
    case "int":
      return int.class;
    case "short":
      return short.class;
    case "long":
      return long.class;
    case "double":
      return double.class;
    case "float":
      return float.class;
    case "boolean":
      return boolean.class;
    case "char":
      return char.class;
    case "byte":
      return byte.class;
    case "void":
      return void.class;
    }
    if (name.endsWith("[]")) {
      return Array.newInstance(nameToClass(name.substring(0, name.length() - 2)), 0).getClass();
    }
    return Class.forName(name);
  }

uncheck is my utility only ignore the exception in lambda, you can simply use try-catch. see source here

then we test our code

  public static void main(String[] args) throws Exception {
    // test normal
    System.out.println(get(new Exception().getStackTrace()[0]));
    // test lambda
    Runnable r = () -> System.out.println(uncheck(() -> get(new Exception().getStackTrace()[0])));
    r.run();
    // test function
    testHere(1);
  }

  private static void testHere(int i) throws Exception {
    System.out.println(get(new Exception().getStackTrace()[0]));
  }

and output

Optional[public static void xdean.stackoverflow.java.reflection.Q44563354.main(java.lang.String[]) throws java.lang.Exception]
Optional[private static java.util.Optional xdean.stackoverflow.java.reflection.Q44563354.lambda$1() throws java.lang.Exception]
Optional[private static void xdean.stackoverflow.java.reflection.Q44563354.testHere(int) throws java.lang.Exception]

But note that this approach only works for class has bytecode in classpath. If you use dynamic proxy, it doesn't work.

Find complete sample code here

EDIT: You can filter synthetic and bridge methods to get the actual method.

like image 103
Dean Xu Avatar answered Oct 06 '22 02:10

Dean Xu