Используйте LambdaMetafactory для вызова метода с одним аргументом для экземпляра класса, полученного из другого загрузчика классов - PullRequest
0 голосов
/ 10 июня 2018

Исходя из этого ответа stackoverflow , я пытаюсь создать экземпляр класса, используя отражение, а затем вызвать метод с одним аргументом для него, используя 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, но не смог понять, что я делаю неправильно.Я надеюсь, что кто-то еще сможет.

Заранее благодарю за помощь.

1 Ответ

0 голосов
/ 11 июня 2018

Экземпляр MethodHandles.Lookup, возвращаемый MethodHandles.lookup(), инкапсулирует контекст вызывающего, то есть контекст вашего класса, который создает новый загрузчик классов.Как говорит исключение, тип Formatter не виден в этом контексте.Вы можете увидеть это как попытку имитировать семантику операции во время компиляции;если вы поместите оператор Formatter.formatSource(sourceText) в свой код, он также не будет работать из-за того, что тип не находится в области видимости.

Вы можете изменить класс контекста объекта поиска, используя in(Class), но при использовании MethodHandles.lookup().in(formatterClass) вы столкнетесь с другой проблемой.Изменение класса контекста объекта поиска снизит уровень доступа, чтобы привести его в соответствие с правилами доступа Java, т.е. вы можете получить доступ только к public членам класса Formatter.Но LambdaMetafactory принимает только объекты поиска, имеющие private доступ к своему классу поиска, то есть объекты поиска, непосредственно созданные самим вызывающим.Единственное исключение будет изменяться между вложенными классами.

Поэтому использование MethodHandles.lookup().in(formatterClass) приводит к Invalid caller: com.google.googlejavaformat.java.Formatter, поскольку вы (вызывающая сторона) не тот класс Formatter.Или, технически, объект поиска не имеет режима доступа private.

Java API не предлагает никакого (простого) способа заставить объект поиска находиться в другом контексте загрузки класса и иметь private доступ (до Java 9).Все обычные механизмы будут включать сотрудничество кода, находящегося в этом контексте.Именно здесь разработчики часто идут по пути создания Reflection с переопределением доступа для манипулирования объектом поиска, чтобы получить желаемые свойства.К сожалению, ожидается, что в будущем новая модульная система станет более строгой, что может нарушить эти решения.

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 с помощью Reflection.Объект поиска инкапсулирует контекст GimmeLookup, который определен в новом URLClassLoader, и достаточен для доступа к public методу formatSource public com.google.googlejavaformat.java.Formatter.

.интерфейс FormatInvoker будет доступен для этого контекста, так как загрузчик классов вашего кода станет родителем созданного URLClassLoader.


Некоторые дополнительные примечания:

  • Конечно, это может стать более эффективным, чем любой другой отражающий доступ, если вы будете использовать сгенерированный экземпляр FormatInvoker достаточно часто, чтобы компенсировать затраты на его создание.

  • Iудалил оператор Thread.currentThread().setContextClassLoader(cl);, так как он не имеет смысла в этой операции, но на самом деле он довольно опасен, поскольку вы не установили его обратно, поэтому поток сохранил ссылку на закрытый URLClassLoader впоследствии.

  • Я упростил вызов toArray до urls.toArray(new URL[0]). Эта статья предоставляет действительно интересное представление о полезности указания размера коллекции для массива.

...