4

基于this stackoverflow answer,我正在尝试使用反射实例化一个类,然后使用它调用一个单参数方法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$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

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

预先感谢您的帮助。

4

1 回答 1

5

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

您可以使用 更改查找对象的上下文类in(Class),但使用 时MethodHandles.lookup().in(formatterClass),您会遇到不同的问题。更改查找对象的上下文类将降低访问级别以使其符合 Java 访问规则,即您只能访问public该类的成员Formatter。但是LambdaMetafactory只接受可以private访问它们的查找类的查找对象,即由调用者本身直接生成的查找对象。唯一的例外是在嵌套类之间进行更改。

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

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

Java 9 提供了一种获取此类查找对象的方法privateLookupIn,它要求目标类位于同一模块中,或者它的模块要对调用者的模块开放以允许此类访问。

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

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

这个子类在构造函数URLClassLoader中调用defineClass一次以添加一个等效于的类

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

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

该接口FormatInvoker将可用于该上下文,因为您的代码的类加载器将成为 created 的父级URLClassLoader


一些附加说明:

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

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

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

于 2018-06-11T17:16:59.247 回答