基于这个堆栈溢出答案,我尝试使用反射实例化一个类,然后使用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 访问规则保持一致,即您只能访问类Formatter
public
成员。但是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 ( "u00CAu00FEu00BAu00BE 001 211 13GimmeLookup7 11 20"
+"java/lang/Object7 31 10<clinit>1 3()V1 4Code1 6lookup1 'Ljav"
+"a/lang/invoke/MethodHandles$Lookup;14 10 1111 2 121 )()Ljava/lang"
+"/invoke/MethodHandles$Lookup;1 36java/lang/invoke/MethodHandles7 1514 "
+"10 1412 16 17261 2 4 12031 10 11 12011 5 "
+"6 1 7 23 3 3 7u00B8 20u00B3 13u00B1 " )
.getBytes(StandardCharsets.ISO_8859_1);
}
此子类URLClassLoader
在构造函数中调用defineClass
一次,以添加等效于
public interface GimmeLookup {
MethodHandles.Lookup lookup = MethodHandles.lookup();
}
然后,代码通过反射读取lookup
字段。查找对象封装了GimmeLookup
的上下文,该上下文在新URLClassLoader
中定义,并且足以访问public
com.google.googlejavaformat.java.Formatter
的public
方法formatSource
。
该上下文可以访问接口FormatInvoker
,因为代码的类加载器将成为创建的URLClassLoader
的父级。
一些附加说明:
当然,这只能比任何其他反射访问更有效,前提是您足够频繁地使用生成的
FormatInvoker
实例来补偿创建它的成本。我删除了
Thread.currentThread().setContextClassLoader(cl);
语句,因为它在此操作中没有任何意义,但实际上很危险,因为您没有将其设置回去,因此线程随后保留了对关闭URLClassLoader
的引用。我简化了对
urls.toArray(new URL[0])
的toArray
调用。本文提供了一个非常有趣的观点,说明为数组指定集合大小的有用性。