使用LambdaMetafactory在从其他类加载器获取的类实例上调用one-arg方法

时间:2018-06-10 19:04:01

标签: java reflection lambda java-8 lambda-metafactory

基于this stackoverflow answer,我试图使用反射实例化一个类,然后使用LambdaMetafactory::metafactory在其上调用一个参数方法(我尝试使用反射,但它相当慢)。< / p>

更具体地说,我想创建一个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

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

提前感谢您的帮助。

1 个答案:

答案 0 :(得分:3)

MethodHandles.Lookup返回的MethodHandles.lookup()实例封装了调用者的上下文,即创建新类加载器的类的上下文。正如例外所述,从此上下文中看不到类型Formatter。您可以将此视为模仿操作的编译时语义的尝试;如果您在代码中放置了语句Formatter.formatSource(sourceText),那么由于类型不在范围内,它也无法正常工作。

您可以使用in(Class)更改查找对象的上下文类,但在使用MethodHandles.lookup().in(formatterClass)时,您将遇到其他问题。更改查找对象的上下文类将降低访问级别以使其与Java访问规则保持一致,即您只能访问类public的{​​{1}}个成员。但是Formatter只接受具有private access的查找对象到它们的查找类,即查询对象直接由调用者自己产生。唯一的例外是在嵌套类之间进行更改。

因此,使用LambdaMetafactory会导致MethodHandles.lookup().in(formatterClass),因为您(调用方)不是Invalid caller: com.google.googlejavaformat.java.Formatter类。或者从技术上讲,查找对象没有Formatter访问模式。

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

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

由于您正在创建新的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

然后,代码通过Reflection读取public interface GimmeLookup { MethodHandles.Lookup lookup = MethodHandles.lookup(); } 字段。查找对象封装了lookup的上下文,该上下文在新GimmeLookup中定义,并且足以访问URLClassLoader public方法formatSource {1}}。

该上下文可以访问接口public,因为您的代码的类加载器将成为创建的com.google.googlejavaformat.java.Formatter的父级。

一些补充说明:

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

  • 我删除了URLClassLoader语句,因为它在此操作中没有任何意义,但实际上是安静的,因为你没有将它设置回来,所以线程保留了对之后关闭FormatInvoker

  • 我简化了Thread.currentThread().setContextClassLoader(cl);URLClassLoader的调用。 This article提供了一个非常有趣的视图,说明了为数组指定集合大小的有用性。