7

I am trying to use Java's LambdaMetaFactory to dynamically implement a generic lambda, Handler<RoutingContext>:

public class RoutingContext {
    // ...
}

@FunctionalInterface
public interface Handler<X> {
    public void handle(X arg);
}

public class HomeHandler extends Handler<RoutingContext> {
    @Override
    public void handle(RoutingContext ctx) {
        // ...
    }
}

Here is my attempt at LambdaMetaFactory:

try {
    Class<?> homeHandlerClass = HomeHandler.class;

    Method method = homeHandlerClass.getDeclaredMethod(
            "handle", RoutingContext.class);
    Lookup lookup = MethodHandles.lookup();
    MethodHandle mh = lookup.unreflect(method);

    MethodType factoryMethodType = MethodType.methodType(Handler.class);
    MethodType functionMethodType = mh.type();
    MethodHandle implementationMethodHandle = mh;

    Handler<RoutingContext> lambda =
            (Handler<RoutingContext>) LambdaMetafactory.metafactory(
                    lookup,
                    "handle",
                    factoryMethodType, 
                    functionMethodType,
                    implementationMethodHandle,
                    implementationMethodHandle.type()) 
            .getTarget()
            .invokeExact();

    lambda.handle(ctx);

} catch (Throwable e) {
    e.printStackTrace();
}

This gives the error:

java.lang.AbstractMethodError: Receiver class [...]$$Lambda$82/0x00000008001fa840
does not define or inherit an implementation of the resolved method abstract
handle(Ljava/lang/Object;)V of interface io.vertx.core.Handler.

I have tried a range of other options for functionMethodType and implementationMethodHandle, but have not managed to get this working yet. Also, even if I replace the RoutingContext.class reference with Object.class, this does not fix the error.

The only way I can get the lambda.handle(ctx) call to succeed is by changing HomeHandler so that it does not extend Handler, making HomeHandler::handle static, and changing RoutingContext.class to Object.class. Oddly I can still cast the resulting lambda to Handler<RoutingContext>, even though it no longer extends Handler.

My questions:

  1. How do I get LambdaMetaFactory to work with non-static methods?

  2. For this non-static SAM class HomeHandler, how does this work with instance allocation under the hood? Does LambdaMetaFactory create a single instance of the interface implementation, no matter how many method calls, since in this example there are no captured variables? Or does it create a new instance for each method call? Or was I supposed to create a single instance and pass it in to the API somehow?

  3. How do I get LambdaMetaFactory to work with generic methods?

Edit: in addition to the great answers below, I came across this blog post explaining the mechanisms involved:

https://medium.freecodecamp.org/a-faster-alternative-to-java-reflection-db6b1e48c33e

Luke Hutchison
  • 8,186
  • 2
  • 45
  • 40

2 Answers2

7

Or was I supposed to create a single instance and pass it in to the API somehow?

Yes. HomeHandler::handle is an instance method, that means you need an instance to create a functional interface wrapper, or pass an instance every time you invoke it (for which Handler won't work as a FunctionalInterface type).

To use a captured instance you should:

  • Change factoryMethodType to also take a HomeHandler instance
  • Change functionMethodType to be the erased type of the SAM, which takes an Object as argument.
  • Change the instantiatedMethodType argument to be the type of the target method handle without the captured HomeHandler instance (since it's captured you don't need it again as a parameter).
  • Pass an instance of HomeHandler to invokeExact when creating the functional interface interface.

-

Class<?> homeHandlerClass = HomeHandler.class;

Method method = homeHandlerClass.getDeclaredMethod(
        "handle", RoutingContext.class);
Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.unreflect(method);

MethodType factoryMethodType = MethodType.methodType(Handler.class, HomeHandler.class);
MethodType functionMethodType = MethodType.methodType(void.class, Object.class);
MethodHandle implementationMethodHandle = mh;

Handler<RoutingContext> lambda =
        (Handler<RoutingContext>) LambdaMetafactory.metafactory(
                lookup,
                "handle",
                factoryMethodType, 
                functionMethodType,
                implementationMethodHandle,
                implementationMethodHandle.type().dropParameterTypes(0, 1)) 
        .getTarget()
        .invokeExact(new HomeHandler()); // capturing instance
lambda.handle(ctx);

Of course, since HomeHandler implements Handler, you could just use the captured instance directly;

new HomeHandler().handle(ctx);

Or leverage the compiler to generate the metafactory code, which also uses invokedynamic, meaning that the CallSite returned by LambdaMetafactory.metafactory will only be created once:

Handler<RoutingContext> lambda = new HomeHandler()::handle;
lambda.handle(ctx);

Or, if the functional interface type is statically know:

MethodHandle theHandle = ...
Object theInstance = ...
MethodHandle adapted = theHandle.bindTo(theInstance);
Handler<RoutingContext> lambda = ctxt -> {
    try {
        adapted.invokeExact(ctxt);
    } catch (Throwable e) {
        throw new RuntimeException(e);
    }
};
lambda.handle(new RoutingContext());
Jorn Vernee
  • 31,735
  • 4
  • 76
  • 93
  • Thanks Jorn for this thorough and clear response. Yes, if this were just one class, I could use one of your last couple of suggestions, but I plan to register Vert.x route handlers dynamically using `LambdaMetaFactory` for all classes in the classpath or module path that are annotated with a `@Route` annotation and that also implement `Handler`, by scanning the classpath with the ClassGraph library. I guess I could just instantiate each matching class, all and cast the instances to `Handler` -- maybe I am looking beyond the mark. – Luke Hutchison Oct 15 '18 at 03:37
  • It's a shame the `LambdaMetaFactory` API is so complex -- I know it is intended for compiler and serialization library writers, but it has a lot of general purpose usecases too. – Luke Hutchison Oct 15 '18 at 03:39
  • 2
    @LukeHutchison I've added another option I think you should be able to use. It looks a little awkward, and the caveat is that you need to know the FI type, since you need that as a target for the lambda expression. But it should be easier to use than `LambdaMetafactory` for going from a `MethodHandle` to a FI wrapper. – Jorn Vernee Oct 15 '18 at 09:51
  • Thanks, that's a really interesting hybrid option. This strikes me as being a "static proxy" similar to how `InvocationHandler` works as a "dynamic proxy". – Luke Hutchison Oct 16 '18 at 22:33
  • 1
    @LukeHutchison speaking of `InvocationHandler` based proxies, such a solution does already exist. It allows implementing an interface almost as one-liner. I added an answer complementing this one. – Holger Oct 17 '18 at 11:14
7

Since you said “it's a shame the LambdaMetaFactory API is so complex”, it should be mentioned it can be done simpler.

First, when using LambdaMetaFactory, use it straight-forwardly:

Lookup lookup = MethodHandles.lookup();
MethodType fType = MethodType.methodType(void.class, RoutingContext.class);
MethodHandle mh = lookup.findVirtual(HomeHandler.class, "handle", fType);

Handler<RoutingContext> lambda = (Handler<RoutingContext>) LambdaMetafactory.metafactory(
    lookup, "handle", MethodType.methodType(Handler.class, HomeHandler.class),
    fType.erase(), mh, fType).getTarget().invokeExact(new HomeHandler());

You are going to invoke an instance method with a bound receiver and the target method’s type excluding the receiver is identical to the instantiatedMethodType parameter. Further, since the bound of T in Handler<T> is Object, you can simply use erase() on that method type to get the erased signature for the samMethodType parameter.

It’s not always that simple. Consider binding a method static int method(int x) to Consumer<Integer>. Then, the samMethodType parameter is (Object)void, the instantiatedMethodType parameter is (Integer)void, whereas the target method’s signature is int(int). You need all these parameters to correctly describe the code to generate. Considering that the other (first three) parameters are normally filled in by the JVM anyway, this method does already require only the necessary minimum.

Second, if you don’t need the maximum performance, you can simply use a Proxy based implementation:

MethodHandle mh = MethodHandles.lookup().findVirtual(HomeHandler.class,
    "handle", MethodType.methodType(void.class, RoutingContext.class));
Handler<RoutingContext> lambda = MethodHandleProxies.asInterfaceInstance(
    Handler.class, mh.bindTo(new HomeHandler()));

This option even exists since Java 7

Holger
  • 285,553
  • 42
  • 434
  • 765