Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct approach to using MethodHandleProxies

In a Java project I'm currently working on, I'm dynamically loading classes then using the reflection API to find and execute methods of those classes that have certain annotations.

The code that performs the actual execution works exclusively in terms of Java-8 functional interfaces (for compatibility reasons), so I need to have an intermediate stage where the Method instances discovered using reflection are converted to appropriate functional interfaces. I achieve this using the MethodHandleProxies class.

For compatibility reasons again, the functional interfaces in question are generic interfaces. This causes an "unchecked conversion" warning when using the MethodHandleProxies.asInterfaceInstance method, since that method returns the "bare" interface.

The following is a brief example that reproduces the main steps involved:

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandleProxies;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.util.Arrays;

public class TestClass {
    private String prefix;

    public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, SecurityException {
        // Use reflection to find method.
        Method method = Arrays.stream(TestClass.class.getDeclaredMethods()) // Stream over methods of ConsumerClass
                .filter(m -> m.isAnnotationPresent(Marker.class)) // Retain only methods with @Marker annotation
                .findFirst().get(); // Get first such method (there is only one in this case)

        // Convert method to "MethodInterface" functional interface.
        MethodHandle handle = MethodHandles.lookup().unreflect(method);
        MethodInterface<TestClass, String> iface = MethodHandleProxies.asInterfaceInstance(MethodInterface.class, handle);

        // Call "testMethod" via functional interface.
        iface.call(new TestClass("A"), "B");
    }

    public TestClass(String prefix) {
        this.prefix = prefix;
    }

    @Marker
    public void testMethod(String arg) {
        System.out.println(prefix + " " + arg);
    }

    @Retention(RUNTIME)
    public @interface Marker { }

    @FunctionalInterface
    public interface MethodInterface<I,V> {
        void call(I instance, V value);
    }
}

This code compiles and runs, but has an unchecked conversion warning on the assignment to iface.

Making MethodInterface non-generic would solve this particular problem, but would mean that it would no longer work with method references for arbitrary types (which is desirable for other parts of the code).

For example, with the above definitions of TestClass and MethodInterface, the following line compiles:

MethodInterface<TestClass,String> iface = TestClass::testMethod;

However, changing to the following definition of MethodInterface breaks this:

@FunctionalInterface
public interface MethodInterface {
    void call(Object inst, Object value);
}

Assigning TestClass::testMethod to an instance of this interface doesn't compile, as the parameters are of the wrong types.

As I see it, I have three options:

  1. Simply live with the warning.
  2. Add an @SuppressWarnings annotation to the assignment.
  3. Come up with an alternative type-safe approach.

I try to ensure there are no warnings generated by my code (to minimise the opportunities for bugs), so I'm not keen on option 1. Option 2 feels like it's simply "papering over the cracks", but is acceptable if absolutely necessary. So my preferred option is to come up with a different approach.

Is there a different approach that is inherently type-safe?

like image 907
Mac Avatar asked Mar 09 '23 01:03

Mac


1 Answers

Assigning a reflectively generated instance to a parameterized generic interface is an unchecked operation, as there is no way to ensure that the generated class fulfills that parameterized interface. In fact, the implementation behind MethodHandleProxies doesn’t care about this signature at all. So having a warning is correct, suppressing it when you are confident that you did everything right, limiting the suppression to the smallest scope possible, is the best (or unavoidable) solution.

You could create a reifiable sub-interface, e.g. interface Specific extends MethodInterface<TestClass,String> {}, use this for the code generation, to have no unchecked operation from the compiler’s point of view, but it wouldn’t change the fact, that the proxy doesn’t really care about the correctness at all.

By the way, if your target interface is a functional interface, you can use LambdaMetafactory instead of MethodHandleProxies. The code generation is slightly more complex, but the resulting class potentially more efficient (even practically in today’s JREs) than the more general proxy.

// Use reflection to find method.
Method method = Arrays.stream(TestClass.class.getDeclaredMethods())
        .filter(m -> m.isAnnotationPresent(Marker.class))
        .findFirst().get();

// Convert method to "MethodInterface" functional interface.
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle handle = lookup.unreflect(method);
MethodInterface<TestClass, String> iface;
try {
    iface = (MethodInterface<TestClass, String>)LambdaMetafactory.metafactory(lookup,
            "call", MethodType.methodType(MethodInterface.class),
            MethodType.methodType(void.class, Object.class, Object.class),
            handle, handle.type())
          .getTarget().invoke();
} catch(RuntimeException|Error|ReflectiveOperationException|LambdaConversionException ex) {
    throw ex;
}
catch (Throwable ex) {
    throw new AssertionError(ex);
}
// Call "testMethod" via functional interface.
iface.call(new TestClass("A"), "B");

It’s just a coincidence that this code doesn’t generate an unchecked warning. It actually bears an unchecked operation, but like the MethodHandleProxies variant, it also has so many other things you can do wrong without the compiler telling you, that it actually doesn’t matter.

like image 90
Holger Avatar answered Mar 20 '23 04:03

Holger