Use LambdaMetafactory to invoke one-arg method on

2019-05-21 13:43发布

问题:

Based on this stackoverflow answer, I am attempting to instantiate a class using reflection and then invoke a one-argument method on it using LambdaMetafactory::metafactory (I tried using reflection, but it was rather slow).

More concretely, I want to create an instance of com.google.googlejavaformat.java.Formatter, and invoke its formatSource() method with the following signature: String formatSource(String input) throws FormatterException.

I have defined the following functional interface:

@FunctionalInterface
public interface FormatInvoker {
  String invoke(String text) throws FormatterException;
}

and am attempting to execute the following code:

try (URLClassLoader cl = new URLClassLoader(urls.toArray(new URL[urls.size()]))) {
  Thread.currentThread().setContextClassLoader(cl);

  Class<?> formatterClass =
      cl.loadClass("com.google.googlejavaformat.java.Formatter");
  Object formatInstance = formatterClass.getConstructor().newInstance();

  Method method = formatterClass.getMethod("formatSource", String.class);
  MethodHandles.Lookup lookup = MethodHandles.lookup();
  MethodHandle methodHandle = lookup.unreflect(method);
  MethodType type = methodHandle.type();
  MethodType factoryType =
      MethodType.methodType(FormatInvoker.class, type.parameterType(0));
  type = type.dropParameterTypes(0, 1);

  FormatInvoker formatInvoker = (FormatInvoker)
    LambdaMetafactory
        .metafactory(
            lookup,
            "invoke",
            factoryType,
            type,
            methodHandle,
            type)
        .getTarget()
        .invoke(formatInstance);

  String text = (String) formatInvoker.invoke(sourceText);
} finally {
  Thread.currentThread().setContextClassLoader(originalClassloader);
}

When I run this code, the call to LambdaMetafactory::metafactory fails with the following exception:

    Caused by: java.lang.invoke.LambdaConversionException: Exception finding constructor
        at java.lang.invoke.InnerClassLambdaMetafactory.buildCallSite(InnerClassLambdaMetafactory.java:229)
        at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:304)
        at com.mycompany.gradle.javaformat.tasks.JavaFormatter.formatSource(JavaFormatter.java:153)
        ... 51 more
    Caused by: java.lang.IllegalAccessException: no such method: com.delphix.gradle.javaformat.tasks.JavaFormatter$$Lambda$20/21898248.get$Lambda(Formatter)FormatInvoker/invokeStatic
        at java.lang.invoke.MemberName.makeAccessException(MemberName.java:867)
        at java.lang.invoke.MemberName$Factory.resolveOrFail(MemberName.java:1003)
        at java.lang.invoke.MethodHandles$Lookup.resolveOrFail(MethodHandles.java:1386)
        at java.lang.invoke.MethodHandles$Lookup.findStatic(MethodHandles.java:780)
        at java.lang.invoke.InnerClassLambdaMetafactory.buildCallSite(InnerClassLambdaMetafactory.java:226)
        ... 53 more
    Caused by: java.lang.LinkageError: bad method type alias: (Formatter)FormatInvoker not visible from class com.delphix.gradle.javaformat.tasks.JavaFormatter$$Lambda$20/21898248
        at java.lang.invoke.MemberName.checkForTypeAlias(MemberName.java:793)
        at java.lang.invoke.MemberName$Factory.resolve(MemberName.java:976)
        at java.lang.invoke.MemberName$Factory.resolveOrFail(MemberName.java:1000)
        ... 56 more

I've read through a number of stackoverflow answers about LambdaMetafactory and read the LambdaMetafactory documentation, but have not been able to figure out what I am doing wrong. I am hoping that somebody else will be able to.

Thank you in advance for your help.

回答1:

The MethodHandles.Lookup instance returned by MethodHandles.lookup() encapsulates the caller’s context, that is, the context of your class which creates the new class loader. As the exception says, the type Formatter is not visible from this context. You can see this as an attempt to mimic the compile-time semantics of the operation; if you placed the statement Formatter.formatSource(sourceText) in your code, it wouldn’t work as well, due to the fact that the type is not in scope.

You can change the context class of the lookup object using in(Class), but when using MethodHandles.lookup().in(formatterClass), you’ll run into a different problem. Changing the context class of a lookup object will reduce the access level to align it with the Java access rules, i.e. you can only access public members of the class Formatter. But the LambdaMetafactory only accepts lookup objects having private access to their lookup class, i.e. lookup objects directly produced by the caller itself. The only exception would be changing between nested classes.

Therefore using MethodHandles.lookup().in(formatterClass) results in Invalid caller: com.google.googlejavaformat.java.Formatter, as you (the caller) are not that Formatter class. Or technically, the lookup object has not the private access mode.

The Java API doesn’t offer any (simple) way to get a lookup object to be in a different class loading context and having the private access (prior to Java 9). All regular mechanisms would involve the cooperation of the code residing in that context. That’s the point where developers often go the route of doing Reflection with access override to manipulate the lookup object, to have the desired properties. Unfortunately, the new module system is expected to become more restrictive in the future, likely breaking these solutions.

Java 9 offers a way to get such a lookup object, privateLookupIn, which requires the target class to be in the same module or its module to be opened to the caller’s module to permit such an access.

Since you are creating a new ClassLoader, you have hands on the class loading context. So, one way to solve the problem, is to add another class to it, which creates the lookup object and allows your calling code to retrieve it:

    try (URLClassLoader cl = new URLClassLoader(urls.toArray(new URL[0])) {
        { byte[] code = gimmeLookupClassDef();
          defineClass("GimmeLookup", code, 0, code.length); }             }) {

        MethodHandles.Lookup lookup = (MethodHandles.Lookup)
            cl.loadClass("GimmeLookup").getField("lookup").get(null);
        Class<?> formatterClass =
            cl.loadClass("com.google.googlejavaformat.java.Formatter");

        Object formatInstance = formatterClass.getConstructor().newInstance();

        Method method = formatterClass.getMethod("formatSource", String.class);
        MethodHandle methodHandle = lookup.unreflect(method);
        MethodType type = methodHandle.type();
        MethodType factoryType =
            MethodType.methodType(FormatInvoker.class, type.parameterType(0));
        type = type.dropParameterTypes(0, 1);

        FormatInvoker formatInvoker = (FormatInvoker)
          LambdaMetafactory.metafactory(
                lookup, "invoke", factoryType, type, methodHandle, type)
            .getTarget().invoke(formatInstance);

      String text = (String) formatInvoker.invoke(sourceText);
      System.out.println(text);
    }
static byte[] gimmeLookupClassDef() {
    return ( "\u00CA\u00FE\u00BA\u00BE\0\0\0001\0\21\1\0\13GimmeLookup\7\0\1\1\0\20"
    +"java/lang/Object\7\0\3\1\0\10<clinit>\1\0\3()V\1\0\4Code\1\0\6lookup\1\0'Ljav"
    +"a/lang/invoke/MethodHandles$Lookup;\14\0\10\0\11\11\0\2\0\12\1\0)()Ljava/lang"
    +"/invoke/MethodHandles$Lookup;\1\0\36java/lang/invoke/MethodHandles\7\0\15\14\0"
    +"\10\0\14\12\0\16\0\17\26\1\0\2\0\4\0\0\0\1\20\31\0\10\0\11\0\0\0\1\20\11\0\5\0"
    +"\6\0\1\0\7\0\0\0\23\0\3\0\3\0\0\0\7\u00B8\0\20\u00B3\0\13\u00B1\0\0\0\0\0\0" )
    .getBytes(StandardCharsets.ISO_8859_1);
}

This subclasses URLClassLoader to call defineClass once in the constructor to add a class being equivalent to

public interface GimmeLookup {
    MethodHandles.Lookup lookup = MethodHandles.lookup();
}

Then, the code reads the lookup field via Reflection. The lookup object encapsulates the context of GimmeLookup, which is defined within the new URLClassLoader, and is sufficient to access the public method formatSource of the public com.google.googlejavaformat.java.Formatter.

The interface FormatInvoker will be accessible for that context, as your code’s class loader will become the parent of the created URLClassLoader.


Some additional notes:

  • Of course, this can only become more efficient than any other reflective access, if you use the generated FormatInvoker instance sufficiently often to compensate for the costs of creating it.

  • I removed the Thread.currentThread().setContextClassLoader(cl); statement, as it has no meaning in this operation, but is, in fact, quiet dangerous as you didn’t set it back, so the thread kept a reference to the closed URLClassLoader afterwards.

  • I simplified the toArray call to urls.toArray(new URL[0]). This article provides a really interesting view on the usefulness of specifying the collection’s size to the array.