使用 LambdaMetafactory 在从其他 classloader 获得的 class 实例上调用一个参数方法

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

基于 ,我试图使用反射实例化一个 class,然后使用 LambdaMetafactory::metafactory 在其上调用一个单参数方法(我尝试使用反射,但它相当慢)。

更具体地说,我想创建一个 com.google.googlejavaformat.java.Formatter 的实例,并使用以下签名调用其 formatSource() 方法:String formatSource(String input) throws FormatterException.

我定义了以下功能接口:

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

并且正在尝试执行以下代码:

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);
}

当我 运行 此代码时,对 LambdaMetafactory::metafactory 的调用失败并出现以下异常:

    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/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/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

我已经通读了一些关于 LambdaMetafactory 的 Whosebug 答案并阅读了 LambdaMetafactory 文档,但无法弄清楚我做错了什么。我希望其他人能够做到。

提前感谢您的帮助。

MethodHandles.lookup() 返回的 MethodHandles.Lookup 实例封装了调用者的上下文,即创建新 class 加载程序的 class 的上下文。如异常所述,类型 Formatter 在此上下文中不可见。您可以将此视为模仿操作的编译时语义的尝试;如果你在你的代码中放置语句 Formatter.formatSource(sourceText),它也不会工作,因为类型不在范围内。

您可以使用 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 将查找对象的上下文 class 更改为其查找 class,即由调用者本身直接生成的查找对象。唯一的例外是嵌套 classes.

之间的变化

因此使用 MethodHandles.lookup().in(formatterClass) 结果是 Invalid caller: com.google.googlejavaformat.java.Formatter,因为您(调用者)不是那个 Formatter class。或者从技术上讲,查找对象没有 private 访问模式。

Java API 没有提供任何(简单的)方法来让查找对象处于不同的 class 加载上下文并具有 private访问(在 Java 9 之前)。所有常规机制都将涉及驻留在该上下文中的代码的合作。这就是开发人员经常采用使用访问覆盖进行反射的方式来操作查找对象,以获得所需属性的地方。不幸的是,新的模块系统预计在未来会变得更加严格,可能会破坏这些解决方案。

Java 9 提供了一种获取此类查找对象的方法,privateLookupIn,这需要目标 class 在同一模块中或其模块要打开给允许此类访问的调用方模块。

由于您正在创建一个新的 ClassLoader,因此您可以处理 class 加载上下文。因此,解决该问题的一种方法是向其添加另一个 class,这将创建查找对象并允许您的调用代码检索它:

    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[=11=][=11=][=11=]01[=11=][=11=]GimmeLookup[=11=][=11=]"
    +"java/lang/Object[=11=][=11=]<clinit>[=11=]()V[=11=]Code[=11=]lookup[=11=]'Ljav"
    +"a/lang/invoke/MethodHandles$Lookup;[=11=][=11=][=11=][=11=][=11=])()Ljava/lang"
    +"/invoke/MethodHandles$Lookup;[=11=]java/lang/invoke/MethodHandles[=11=][=11=]"
    +"[=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=]"
    +"[=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=][=11=]\u00B8[=11=]\u00B3[=11=]\u00B1[=11=][=11=][=11=][=11=][=11=][=11=]" )
    .getBytes(StandardCharsets.ISO_8859_1);
}

这个subclasses URLClassLoader在构造函数中调用defineClass一次添加一个class相当于

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

然后,代码通过反射读取 lookup 字段。查找对象封装了 GimmeLookup 的上下文,它定义在新的 URLClassLoader 中,足以访问 public formatSourcepublic 方法com.google.googlejavaformat.java.Formatter.

该上下文可以访问接口 FormatInvoker,因为您代码的 class 加载器将成为创建的 URLClassLoader.

的父级

一些补充说明:

  • 当然,如果您足够频繁地使用生成的 FormatInvoker 实例来补偿创建它的成本,这只会比任何其他反射访问更有效。

  • 我删除了 Thread.currentThread().setContextClassLoader(cl); 语句,因为它在此操作中没有任何意义,但事实上,由于您没有将其设置回去,因此非常危险,因此线程保持之后对关闭的 URLClassLoader 的引用。

  • 我将 toArray 调用简化为 urls.toArray(new URL[0])This article 提供了一个非常有趣的观点,说明了为数组指定集合大小的有用性。