6

I want to capture calls to a mock object

public interface Service {
    public String stringify(Object o);
}
service = mockery.mock(Service.class);
mockery.allowing(service::stringify).with(42).will(() -> "42");

So inside allowing I have a Function<Object, String>

Is there any reflecto-magic that will let me find the service from the function created from the method reference?

public WithClause allowing(Function<T,R> f) {
    Object myServiceBackAgain = findTargetOf(function);
    ....
}

I know that the Function will always come from these method references, so I'm happy to down-cast as much as is necessary.

This is not the same question as the related Is it possible to convert method reference to MethodHandle? because, well for a start it isn't the same question, just in a related area. And even if I can get a MethodHandle, I can't get the target from it.

Community
  • 1
  • 1
Duncan McGregor
  • 17,665
  • 12
  • 64
  • 118
  • 1
    Nobody said that this is the same question. Maybe you should read the message: “*This question already has an answer here*”. So start reading the *answer*. It explains everything, so you don’t need to speculate whether you can get a MethodHandle, that’s not the point. It’s *only a title*. – Holger Jul 02 '15 at 19:15
  • Perhaps @Holger, maybe as an answer to this question, you would like to explain how the answer to the linked question answers mine? In particular where the Function is not serializable. – Duncan McGregor Jul 02 '15 at 21:41
  • 2
    basically it explains that it’s not possible (besides the Serialization trick) and even if it was, it didn’t do what you expect as there is no guaranty that a method reference in your source code ends up being a direct handle on the byte code level. Note further, that there are plenty of links to related questions for additional information. That’s why I directed to that question instead of directly to [“How to get the MethodInfo of a Java 8 method reference?”](http://stackoverflow.com/q/19845213/2711488) which is closer to your wording but provides less information in the answers. – Holger Jul 03 '15 at 08:21
  • 1
    Still refusing to provide an answer to a good question on a question and answer site? I'm sorry, but closing this question, which does not have an answer without chasing links and making inferences, just because you have provided an answer to something like it, is silly. Please do the decent thing and reopen it. – Duncan McGregor Jul 03 '15 at 08:49
  • I want an answer here, as an answer, and I don't want you to close my question. And note that your answer, to another question, may not be correct. The very least you can do is to allow other people to have a crack at this one. – Duncan McGregor Jul 03 '15 at 13:27
  • 1
    *especially* if you assume that that answer isn’t correct, it should stand corrected *there* instead of here. That’s why on Stackoverflow questions are closed when another one has an answer, because there should be a single point to look for. Nevertheless, you *have* an answer here. All you have to do is to accept it. – Holger Jul 03 '15 at 13:35
  • Sigh. It may be possible to find the target of a method reference even if you can't convert a method reference to MethodHandle. They are different questions. I'm grateful for the answer here, but your closing the question is preventing other people from suggesting potential solutions to my problem. In particular me, as I may have a way of getting the target. So please reopen this question so that people can answer it, not a related question but fundamentally different question. – Duncan McGregor Jul 03 '15 at 14:46
  • 1
    *Don’t stop reading at the title*. Neither the question nor the answer are about MethodHandles. – Holger Jul 03 '15 at 15:02

2 Answers2

9

Using the trick from this SO post you can find the target. The important method below is findTarget. As it turns out, lambdas do indeed capture their targets, and you can access them from the SerializedLambda.

However, this is a pretty nasty reflection hack and it's likely to break in future versions. I do not condone its usage.

import java.io.Serializable;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.function.Function;

public class FindMethodReferenceTarget {
  public static void main(String[] args) {
    String s = "123";
    Optional<Object> target = findTarget(s::charAt);
    System.out.println(target.get().equals(s));

    Object o = new FindMethodReferenceTarget();
    target = findTarget(o::equals);
    System.out.println(target.get().equals(o));
  }

  private static <T, R> Optional<Object> findTarget(
      DebuggableFunction<T, R> methodReference) {
    return getLambda(methodReference).map(l -> l.getCapturedArg(0));
  }

  private static Optional<SerializedLambda> getLambda(Serializable lambda) {
    for (Class<?> cl = lambda.getClass(); cl != null; cl = cl.getSuperclass()) {
      try {
        Method m = cl.getDeclaredMethod("writeReplace");
        m.setAccessible(true);
        Object replacement = m.invoke(lambda);
        if (!(replacement instanceof SerializedLambda)) {
          break; // custom interface implementation
        }
        SerializedLambda l = (SerializedLambda) replacement;
        return Optional.of(l);
      } catch (NoSuchMethodException e) {
        // do nothing
      } catch (IllegalAccessException | InvocationTargetException e) {
        break;
      }
    }

    return Optional.empty();
  }

  @FunctionalInterface
  private static interface DebuggableFunction<T, R> extends
      Serializable,
      Function<T, R> {}
}
Community
  • 1
  • 1
Jeffrey
  • 44,417
  • 8
  • 90
  • 141
0

There's no direct way to find the target, because method references just get translated to lambdas (which are, by definition, anonymous) under the covers. So you'll need to use a workaround.

Presumably you're familiar with Java 7's proxies, since you've managed to implement your mock factory method.

The workaround then is that when someone invokes your allowing method, you set some sort of global flag to alert all your mocks that you want to record the next call, and then you invoke the lambda you were given. By seeing which mock recorded the call, you've now found the target of the method reference, and you can unset the global flag and proceed on with the rest of your mocking framework.

It's ugly, I know.

Nebu Pookins
  • 318
  • 1
  • 6
  • Yeah, that's what I'm doing at the moment! I am still hoping that the anonymity only applies if you don't know what to down-cast too - after all the implementation of the interface is a concrete thing that must know it's target. – Duncan McGregor Jul 02 '15 at 07:54
  • @DuncanMcGregor not necessarily; the JVM might implement the lambda as a closure (and not a method on some object) where the instance of `Service` which acts as a the receiver is essentially a local variable from the perspective of the implementation of the lambda (and there's no way to introspect the local variables of an arbitrary function from outside that function). In the worst case, the function might not even be java but instead a call to native (e.g. C) code. I don't know that that's what the JVM actually does, but I think they've set up the API/contract so that they could do this. – Nebu Pookins Jul 02 '15 at 08:01
  • I agree that there are many ways that a Lambda might be implemented - but in this case, the JVM must be holding a reference to the object in order to do the call right? – Duncan McGregor Jul 02 '15 at 08:04