4

If a method in a library is called with a Java Lambda expression, these are often just wrapped method calls. Is it possible to find out what method that originally was, just for logging purposes? (Another question is about what object it applies to - this is specifically about the called method.)

class Foo {
    private void doSomething() { ... }

    public void doSomethingInTransaction() {
        doInTransaction(this::doSomething);
    }

    private void doInTransaction(Runnable run) { ... } 
}

When calling doSomethingInTransaction() the method doInTransaction is actually called with an Object of type Runnable. It would sometimes be nice to log the name and class of the method that was passed here (that is, Foo.doSomething), as well as the object. Is it somehow possible to find out what that was via reflection or whatnot? If that requires specific Java versions, that'd be an interesting answer, too.

(UPDATE: please note that this is not a duplicate of the related question Java 8 - how to access object and method encapsulated as lambda since I'm mostly asking for the method that is encapsulated there. That wasn't asked there.)

Dr. Hans-Peter Störr
  • 25,298
  • 30
  • 102
  • 139
  • 5
    `doInTransaction(this::doSomething());` will not compile. There is a reason I removed the `()`. – luk2302 May 17 '19 at 13:44
  • 1
    Also missing the return type for `private void doInTransaction`. – sfiss May 17 '19 at 13:52
  • Functional interfaces only have one method, so probably with a bit of Java reflection this can be done! – Edwin Dalorzo May 17 '19 at 14:02
  • 1
    Is it a duplicate? The linked answer only supplies the solution to get the defining class, in this case `Foo`, and not the method name `doSomething` (which is not possible by those means). – sfiss May 17 '19 at 14:19
  • @sfiss you are right - I am asking specifically about the method, and the other question does not ask for that nor has an answer that talks about this. It seems people here are sometimes too quick to close questions - that's not the first time something like that happened to me. :-( – Dr. Hans-Peter Störr May 17 '19 at 16:22
  • That might be the solution: https://stackoverflow.com/a/31237349/21499 . – Dr. Hans-Peter Störr May 22 '19 at 13:43

1 Answers1

2

The following example shows how to get the method reference name from the runnable. As explained in the comments, the code may be unnecesserarily complex and only works for certain cases (including the one in the question). Also, it makes certain assumptions that don't work in the general case.

Example class:

public class Test {

    public void callingMethod() {
        this.acceptingMethod(this::methodReferenceMethod);
    }

    public void acceptingMethod(final Runnable runnable) {
        final String name = Util.getRunnableName(runnable, "acceptingMethod");
        System.out.println("Name is " + name);
    }

    public void methodReferenceMethod() {

    }

    public static void main(final String[] args) {
        new Test().callingMethod();
    }

}

Now the actual magic here:

class Util {

public static String getRunnableName(final Runnable runnable, final String calledMethodName) {
    final String callSiteMethodName = getCallSiteMethodNameNotThreadSafe();
    final Class<?> callSiteClass = getDeclaringClass(runnable);
    final String runnableName = extractRunnableName(callSiteClass, callSiteMethodName, calledMethodName);
    return runnableName;
}

private static String extractRunnableName(
        final Class<?> callSiteClass,
        final String callSiteMethodName,
        final String calledMethodName) {
    try {
        final AtomicReference<String> result = new AtomicReference<>(null);
        final ClassReader cr = new ClassReader(callSiteClass.getName());
        final TraceClassVisitor traceVisitor = new TraceClassVisitor(new PrintWriter(System.out));
        cr.accept(new CheckClassAdapter(Opcodes.ASM7, traceVisitor, false) {

            @Override
            public MethodVisitor visitMethod(final int access, final String name, final String descriptor, final String signature, final String[] exceptions) {
                if (!name.equals(callSiteMethodName)) {
                    return super.visitMethod(access, calledMethodName, descriptor, signature, exceptions);
                }

                return new CheckMethodAdapter(Opcodes.ASM7, super.visitMethod(access, name, descriptor, signature, exceptions), new HashMap<>()) {

                    @Override
                    public void visitInvokeDynamicInsn(final String name, final String descriptor, final Handle bootstrapMethodHandle, final Object... bootstrapMethodArguments) {
                        final String invokeDynamic = ((Handle) bootstrapMethodArguments[1]).getName();
                        result.set(invokeDynamic);
                    }

                };
            }

        }, 0);
        return result.get();
    } catch (final IOException e) {
        throw new RuntimeException(e);
    }
}

public static String getCallSiteMethodNameNotThreadSafe() {
    final int depth = 4;
    return Thread.currentThread().getStackTrace()[depth].getMethodName();
}

public static Class<?> getDeclaringClass(final Runnable runnable) {
    return Arrays.stream(runnable.getClass().getDeclaredFields())
            .filter(f -> f.getName().equals("arg$1"))
            .map(f -> {
                f.setAccessible(true);
                try {
                    return f.get(runnable).getClass();
                } catch (IllegalArgumentException | IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            })
            .findFirst()
            .orElseThrow(IllegalStateException::new);
}

}

The output is as expected "Name is methodReferenceMethod". I would probably never use this in any project, but I guess it is possible. Also, this only works for the given example, as there is only one INVOKEVIRTUAL in the calling method. For the general case, one would need to adjust the checkMethodVisitor and filter the calls to the "calledMethodName" only. Lastly, the code to get the calling method uses a fixed index for the stack trace element, which also does not generalize well.

sfiss
  • 2,119
  • 13
  • 19
  • There is no need to use a `TraceClassVisitor`, `CheckClassAdapter`, nor `CheckMethodAdapter` here. Just creating simple subclasses of `ClassVisitor` and `MethodVisitor` would be enough. Likewise, `Arrays.stream(runnable.getClass().getDeclaredFields()) .filter(f -> f.getName().equals("arg$1"))` is quiet convoluted compare to `runnable.getClass().getDeclaredField("arg$1")`—not that assuming that there has to be an `arg$1` field was portable in anyway. Even for this implementation, it only works if the method reference has the form `this::name` rather than `someObject::name` or `ClassName::name`. – Holger May 22 '19 at 08:59
  • Other things you’re implicitly relying on, are that the caller always is also the creation site and that the particular method is not overloaded, as you’re only making a name check, further, the method must not contain other method references or lambda expressions. Besides that, why do you think, `Thread.currentThread().getStackTrace()` is not thread safe? – Holger May 22 '19 at 09:09
  • @Holger: those are all valid points. I only worked on it so that it works on the provided example to show that it could be somehow possible. I made an edit to explain that it only works in certain cases. – sfiss May 22 '19 at 09:52
  • Thank you! Impressive. It might be a good idea to insert the relevant imports (even if only as wildcard import) so that people can see easier what libraries you are using. – Dr. Hans-Peter Störr May 22 '19 at 13:35