Use LambdaMetafactory to invoke one-arg method on class instance obtained from other classloader

前端 未结 1 1350
渐次进展
渐次进展 2021-01-13 15:53

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::metafa

相关标签:
1条回答
  • 2021-01-13 16:42

    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.

    0 讨论(0)
提交回复
热议问题