Экземпляр 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])
. Эта статья предоставляет действительно интересное представление о полезности указания размера коллекции для массива.