Create BiConsumer from LambdaMetafactory

2020-07-29 23:20发布

问题:

I'm trying to dynamically create a method reference of type BiConsumer through LambdaMetafactory. I was trying to apply two approaches found on https://www.cuba-platform.com/blog/think-twice-before-using-reflection/ - createVoidHandlerLambda and here Create BiConsumer as Field setter without reflection the Holger's answer.

However in both cases I'm having below error:

Exception in thread "main" java.lang.AbstractMethodError: Receiver class org.home.ref.App$$Lambda$15/0x0000000800066040 does not define or inherit an implementation of the resolved method abstract accept(Ljava/lang/Object;Ljava/lang/Object;)V of interface java.util.function.BiConsumer.
    at org.home.ref.App.main(App.java:20)

My code is something like this:

public class App {

    public static void main(String[] args) throws Throwable {
        MyClass myClass = new MyClass();
        BiConsumer<MyClass, Boolean> setValid = MyClass::setValid;
        setValid.accept(myClass, true);

        BiConsumer<MyClass, Boolean> mappingMethodReferences = createHandlerLambda(MyClass.class);
        mappingMethodReferences.accept(myClass, true);
    }

    @SuppressWarnings("unchecked")
    public static BiConsumer<MyClass, Boolean> createHandlerLambda(Class<?> classType) throws Throwable {
        Method method = classType.getMethod("setValid", boolean.class);
        MethodHandles.Lookup caller = MethodHandles.lookup();
        CallSite site = LambdaMetafactory.metafactory(caller,
                "accept",
                MethodType.methodType(BiConsumer.class),
                MethodType.methodType(void.class, MyClass.class, boolean.class),
                caller.findVirtual(classType, method.getName(),
                        MethodType.methodType(void.class, method.getParameterTypes()[0])),
                MethodType.methodType(void.class, classType, method.getParameterTypes()[0]));

        MethodHandle factory = site.getTarget();
        return (BiConsumer<MyClass, Boolean>) factory.invoke();
    }

    public static <C, V> BiConsumer<C, V> createSetter(Class<?> classType) throws Throwable {
        Field field = classType.getDeclaredField("valid");
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        final MethodHandle setter = lookup.unreflectSetter(field);
        final CallSite site = LambdaMetafactory.metafactory(lookup,
                "accept", MethodType.methodType(BiConsumer.class, MethodHandle.class),
                setter.type().erase(), MethodHandles.exactInvoker(setter.type()), setter.type());
        return (BiConsumer<C, V>)site.getTarget().invokeExact(setter);
    }

}

Where MyClass looks like this:

public class MyClass {

    public boolean valid;

    public void setValid(boolean valid) {
        this.valid = valid;
        System.out.println("Called setValid");
    }
}

I will appreciate for help with this one.

EDIT #1. After consulting @Holger I've modified createSetter method to:

@SuppressWarnings("unchecked")
    public static <C, V> BiConsumer<C, V> createSetter(Class<?> classType) throws Throwable {
        Field field = classType.getDeclaredField("valid");
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        final MethodHandle setter = lookup.unreflectSetter(field);
        MethodType type = setter.type();
        if(field.getType().isPrimitive())
            type = type.wrap().changeReturnType(void.class);
        final CallSite site = LambdaMetafactory.metafactory(lookup,
                "accept", MethodType.methodType(BiConsumer.class, MethodHandle.class),
                type.erase(), MethodHandles.exactInvoker(setter.type()), type);
        return (BiConsumer<C, V>)site.getTarget().invokeExact(setter);
    }

Now this method does not throw the initial Exception althoug it seems that calling accept on this method reference has no effect. I do not see "Called setValid" in logs for this call. Only for MyClass::setValid;

回答1:

Note that your use of getMethod and caller.findVirtual(…) for the same method is redundant. If your starting point is a Method, you may use unreflect, e.g.

Method method = classType.getMethod("setValid", boolean.class);
MethodHandles.Lookup caller = MethodHandles.lookup();
MethodHandle target = caller.unreflect(method);

This might be useful when you discover methods dynamically and/or are looking for other artifacts like annotations in the process. Otherwise, just getting the MethodHandle via findVirtual is enough.

Then, you have to understand the three different function types:

  • The target method handle has a specific type which is given implicitly when passing the method handle to the factory. In your case, it is (MyClass,boolean) → void
  • The generic function type associated with the intended result type
    BiConsumer<MyClass, Boolean>, which is (MyClass,Boolean) → void
  • The erased type of the BiConsumer interface, which is (Object,Object) → void

Only specifying all three types correctly tells the factory that it must implement the method
void accept(Object,Object) with code which will cast the first argument to MyClass and the second to Boolean, followed by unwrapping the second argument to boolean, to eventually invoke the target method.

We could specify the types explicitly, but to make the code as reusable as possible, we can call type() on the target, followed by using adapter methods.

  • wrap() will convert all primitive types to their wrapper type. Unfortunately, this also implies converting the return type to Void, so we have to set it back to void again.
    This gives us the instantiatedMethodType parameter. (Compare with the documentation)
  • erase() will convert all reference types to Object but leave all primitive types as-is. So applying it to the instantiatedMethodType gives us the erased type.
    It depends on the particular target interface whether this simple transformation is sufficient. For the interfaces in java.util.function, it is.

Another point to raise the reusability is to use an actual type parameter for the method receiver class, as we get the class as parameter anyway:

public static <T>
       BiConsumer<T, Boolean> createHandlerLambda(Class<T> classType) throws Throwable {

    MethodHandles.Lookup caller = MethodHandles.lookup();
    MethodHandle target = caller.findVirtual(classType, "setValid",
        MethodType.methodType(void.class, boolean.class));
    MethodType instantiated = target.type().wrap().changeReturnType(void.class);

    CallSite site = LambdaMetafactory.metafactory(caller,
            "accept", MethodType.methodType(BiConsumer.class),
            instantiated.erase(), target, instantiated);
    return (BiConsumer<T, Boolean>)site.getTarget().invoke();
}