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



基于这个堆栈溢出答案,我尝试使用反射实例化一个类,然后使用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

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

提前感谢您的帮助。

MethodHandles.lookup()返回的MethodHandles.Lookup实例封装调用方的上下文,即创建新类装入器的类的上下文。正如异常所述,在此上下文中看不到类型Formatter。您可以将其视为模拟操作的编译时语义的尝试;如果将语句放在代码中Formatter.formatSource(sourceText),则由于类型不在范围内,它也不会正常工作。

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

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

JavaAPI 不提供任何(简单(方法来获取查找对象位于不同的类加载上下文中并具有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 ( "u00CAu00FEu00BAu00BE00121113GimmeLookup71120"
+"java/lang/Object73110<clinit>13()V14Code16lookup1'Ljav"
+"a/lang/invoke/MethodHandles$Lookup;141011112121)()Ljava/lang"
+"/invoke/MethodHandles$Lookup;136java/lang/invoke/MethodHandles71514"
+"101412161726124120311011120115"
+"61723337u00B820u00B313u00B1" )
.getBytes(StandardCharsets.ISO_8859_1);
}

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

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

然后,代码通过反射读取lookup字段。查找对象封装了GimmeLookup的上下文,该上下文在新URLClassLoader中定义,并且足以访问publiccom.google.googlejavaformat.java.Formatterpublic方法formatSource

该上下文可以访问接口FormatInvoker,因为代码的类加载器将成为创建的URLClassLoader的父级。


一些附加说明:

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

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

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

最新更新