5

Despite having read all the documentation I'm aware of, I cannot resolve an issue with using lambdas to execute a method. To give a bit of background my use case is a plugin system. I'm using an annotation (@EventHandle) which can be assigned to any method. I use reflection and iterate through every method in the class and check if it has the annotation, if it does the method is added to a handler object (which is added to a list for processing every "tick"). Here is my handler class:

package me.b3nw.dev.Events;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.lang.invoke.*;
import java.lang.reflect.Method;
import java.lang.reflect.Type;

@Slf4j
public class Handler {

    @Getter
    private final Method method;
    @Getter
    private final EventHandle handle;
    private final MethodHandles.Lookup lookup;
    private final MethodHandle methodHandle;
    private final EventHandler invoker;

    public Handler(Method method, EventHandle handle) throws Throwable {
        this.method = method;

        log.info(method.getGenericReturnType() + "");

        for(Type type : method.getParameterTypes()) {
            log.info(type.getTypeName());
        }

        this.handle = handle;
        this.lookup = MethodHandles.lookup();
        this.methodHandle = lookup.unreflect(method);

        log.info("" + methodHandle.type().toMethodDescriptorString());

        this.invoker = (EventHandler) LambdaMetafactory.metafactory(lookup, "handle", MethodType.methodType(EventHandler.class), methodHandle.type(), methodHandle, methodHandle.type()).getTarget().invokeExact();
    }

    public void invoke(GameEvent evt) throws Throwable {
        invoker.handle(evt);
    }

}

In the current iteration of this class I'm casting straight to the functional interface EventHandler, source:

package me.b3nw.dev.Events;

@FunctionalInterface
public interface EventHandler {

    boolean handle(GameEvent evt);

}

Currently I get the following error:

ERROR   m.b.d.H.GamemodeHandler - 
java.lang.AbstractMethodError: me.b3nw.dev.Events.Handler$$Lambda$3/1704984363.handle(Lme/b3nw/dev/Events/GameEvent;)Z
    at me.b3nw.dev.Events.Handler.invoke(Handler.java:40) ~[classes/:na]
    at me.b3nw.dev.Handlers.GamemodeHandler.userEventTriggered(GamemodeHandler.java:34) ~[classes/:na]

GamemodeHandler just calls the invoke method in Handler class.

So it outputs an AbstractMethodError when I cast straight to EventHandler and execute, when I don't cast it I get a different error which is:

java.lang.invoke.WrongMethodTypeException: expected ()EventHandler but found ()void
    at java.lang.invoke.Invokers.newWrongMethodTypeException(Invokers.java:294) ~[na:1.8.0_45]
    at java.lang.invoke.Invokers.checkExactType(Invokers.java:305) ~[na:1.8.0_45]
    at me.b3nw.dev.Events.Handler.invoke(Handler.java:40) ~[classes/:na]
    at me.b3nw.dev.Handlers.GamemodeHandler.userEventTriggered(GamemodeHandler.java:34) ~[classes/:na]

The modified Handler to reflect changes:

package me.b3nw.dev.Events;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.lang.invoke.*;
import java.lang.reflect.Method;
import java.lang.reflect.Type;

@Slf4j
public class Handler {

    @Getter
    private final Method method;
    @Getter
    private final EventHandle handle;
    private final MethodHandles.Lookup lookup;
    private final MethodHandle methodHandle;
    private final MethodHandle invoker;

    public Handler(Method method, EventHandle handle) throws Throwable {
        this.method = method;

        log.info(method.getGenericReturnType() + "");

        for(Type type : method.getParameterTypes()) {
            log.info(type.getTypeName());
        }

        this.handle = handle;
        this.lookup = MethodHandles.lookup();
        this.methodHandle = lookup.unreflect(method);

        log.info("" + methodHandle.type().toMethodDescriptorString());

        this.invoker = LambdaMetafactory.metafactory(lookup, "handle", MethodType.methodType(EventHandler.class), methodHandle.type(), methodHandle, methodHandle.type()).getTarget();
    }

    public void invoke(GameEvent evt) throws Throwable {
        invoker.invokeExact();
    }

}

This class has a method which is annotated and should implement the signature of the functional interface but.. clearly not :( Here's the class:

package me.b3nw.dev.Gamemode;

import lombok.extern.slf4j.Slf4j;
import me.b3nw.dev.Events.EventHandle;
import me.b3nw.dev.Events.GameEvent;

@Slf4j
public class Vanilla extends Gamemode {

    public void testMethod() {

    }

    @EventHandle(type = EventHandle.Type.NICKANNOUNCE)
    public boolean testMethod2(GameEvent evt) {
        log.info("Fuck yeah!"/* + evt*/);
        return true;
    }

}

How do I go about fixing this, am I using lambdas completely wrong here?

Thank you.

assylias
  • 321,522
  • 82
  • 660
  • 783
B3NW
  • 163
  • 1
  • 2
  • 8
  • Why are you using LambdaMetafactory? It seems you could just use an actual Java lambda that calls invokeExact. – Jeffrey Bosboom Jun 07 '15 at 16:31
  • could you explain that further @JeffreyBosboom please? I have experience with reflection but other than using a lambda expression I've not really used them much. Thanks :) – B3NW Jun 07 '15 at 16:34
  • 2
    You have a MethodHandle `h`, you want an `EventHandler` object that calls `h.invokeExact` passing the event as an argument. So write `EventHandler handler = (event) -> h.invokeExact(event);`. (Well, you need a try/catch block because invokeExact `throws Throwable`.) `javac` will emit a metafactory call here to create the lambda -- you don't need to do it manually. – Jeffrey Bosboom Jun 07 '15 at 16:36
  • @JeffreyBosboom I've given that way a go, although I'm pretty sure when done that way it takes a performance hit (calling invokeexact every time). I get no exceptions but I'm not getting the log message "Fuck yeah!" either. – B3NW Jun 07 '15 at 16:57
  • Current invoke code only outputs executing 1, it doesn't get to the second.. no exceptions printed. ` log.info("Executing! 1"); EventHandler handler = (event) -> { try { log.info("Executing! 2"); methodHandle.invokeExact(evt); return true; } catch (Throwable throwable) { log.error("", throwable); } return true; };` – B3NW Jun 07 '15 at 17:03
  • 2
    If you're not writing a compiler, you are almost certainly looking in the wrong place. – Brian Goetz Jun 09 '15 at 08:43
  • @BrianGoetz I'm not writing a compiler, however this is the most efficient and developer friendly way. I could have done it with reflection with ease but as we know.. reflection is expensive and can be even more so when done a lot. This method uses reflection only in the initialisation of a pipeline and uses the lambda to invoke the method, that way I have a cheap way to invoke on every tick. – B3NW Jun 09 '15 at 12:49
  • 1
    If you are just looking to invoke the method, don't bother wrapping the MH with a functional interface -- just invoke the MH! You are making life harder for yourself than you need to. – Brian Goetz Jun 10 '15 at 19:14

1 Answers1

4

If you looked at your log output you noticed that your target method signature looks like (Lme/b3nw/dev/Events/Vanilla;Lme/b3nw/dev/Events/GameEvent;)Z, in other words, since your target method is an instance method, it needs an instance of its class (i.e. Vanilla) as first argument.

If you don’t provide an instance at lambda creation time but pass the target method’s signature as functional signature, the created lambda instance will have a method like

boolean handle(Vanilla instance, GameEvent evt) { instance.testMethod2(evt); }

which doesn’t match the real interface method

boolean handle(GameEvent evt);

which you are trying to invoke. Therefore you get an AbstractMethodError. The LambdaMetaFactory is for compiler generated code in the first place and doesn’t perform expensive checks, i.e. doesn’t try to determine the functional interface method to compare it with the provided signature.

So what you have to do is to provide the instance on which the target method ought to be invoked:

public Handler(Method method, Object instance, EventHandle handle) throws Throwable {
    this.method = method;

    log.info(method.getGenericReturnType() + "");

    for(Type type : method.getParameterTypes()) {
        log.info(type.getTypeName());
    }

    this.handle = handle;
    this.lookup = MethodHandles.lookup();
    this.methodHandle = lookup.unreflect(method);
    MethodType type = methodHandle.type();
    // add the type of the instance to the factory method
    MethodType factoryType=MethodType.methodType(EventHandler.class,type.parameterType(0));
    // and remove it from the function signature
    type=type.dropParameterTypes(0, 1);

    log.info("" + type.toMethodDescriptorString());

    this.invoker = (EventHandler)LambdaMetafactory.metafactory(lookup,
        "handle", factoryType, type, methodHandle, type).getTarget()
    // use invoke instead of invokeExact as instance is declared as Object
        .invoke(instance);
}

Of course, you also have to adapt the code which gathers the annotated methods to pass through the instance which you are working on.

Note that this all refers to your first version. I couldn’t get the point of your “modified Handler”.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Hey dude I need your name so I can call my first born after you! Haha. I wondered why the method signature needed an instance, I thought it may have been the issue and tried dropping parameters but received an error every time, my code works flawlessly now, I'll leave a comment in the code so when I release publicly it has a thank you. Much appreciated! :) – B3NW Jun 08 '15 at 18:19