为什么我无法在这里使用.invokeExact(),即使MethodType是正确的?

16

我需要为我的一个项目实现动态构造函数调用。但由于使用的是Java 7,所以我使用java.lang.invoke而不是“传统”的反射API。

代码:

@ParametersAreNonnullByDefault
public class PathMatcherProvider
{
    private static final MethodHandles.Lookup LOOKUP
        = MethodHandles.publicLookup();
    private static final MethodType CONSTRUCTOR_TYPE
        = MethodType.methodType(void.class, String.class);

    private final Map<String, Class<? extends PathMatcher>> classMap
        = new HashMap<>();
    private final Map<Class<? extends PathMatcher>, MethodHandle> handleMap
        = new HashMap<>();

    public PathMatcherProvider()
    {
        registerPathMatcher("glob", GlobPathMatcher.class);
        registerPathMatcher("regex", RegexPathMatcher.class);
    }

    public final PathMatcher getPathMatcher(final String name, final String arg)
    {
        Objects.requireNonNull(name);
        Objects.requireNonNull(arg);

        final Class<? extends PathMatcher> c = classMap.get(name);
        if (c == null)
            throw new UnsupportedOperationException();

        try {
            return c.cast(handleMap.get(c).invoke(arg));
        } catch (Throwable throwable) {
            throw new RuntimeException("Unhandled exception", throwable);
        }
    }

    protected final void registerPathMatcher(@Nonnull final String name,
        @Nonnull final Class<? extends PathMatcher> matcherClass)
    {
        Objects.requireNonNull(name);
        Objects.requireNonNull(matcherClass);
        try {
            classMap.put(name, matcherClass);
            handleMap.put(matcherClass, findConstructor(matcherClass));
        } catch (NoSuchMethodException | IllegalAccessException e) {
            throw new RuntimeException("cannot find constructor", e);
        }
    }

    private static <T extends PathMatcher> MethodHandle findConstructor(
        final Class<T> matcherClass)
        throws NoSuchMethodException, IllegalAccessException
    {
        Objects.requireNonNull(matcherClass);
        return LOOKUP.findConstructor(matcherClass, CONSTRUCTOR_TYPE);
    }

    public static void main(final String... args)
    {
        new PathMatcherProvider().getPathMatcher("regex", "^a");
    }
}

好的,这样可行。

我遇到的问题出现在这一行:

return c.cast(handleMap.get(c).invoke(arg));

如果我用invokeExact替换invoke,我会得到以下堆栈跟踪:
Exception in thread "main" java.lang.RuntimeException: Unhandled exception
    at com.github.fge.filesystem.path.matchers.PathMatcherProvider.getPathMatcher(PathMatcherProvider.java:62)
    at com.github.fge.filesystem.path.matchers.PathMatcherProvider.main(PathMatcherProvider.java:89)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
Caused by: java.lang.invoke.WrongMethodTypeException: expected (String)RegexPathMatcher but found (String)Object
    at java.lang.invoke.Invokers.newWrongMethodTypeException(Invokers.java:350)
    at java.lang.invoke.Invokers.checkExactType(Invokers.java:361)
    at com.github.fge.filesystem.path.matchers.PathMatcherProvider.getPathMatcher(PathMatcherProvider.java:60)

我还不太明白。无论是 GlobPathMatcher 还是 RegexPathMatcher,它们都使用一个带有 String 参数的单个构造函数,因此两者的 MethodType 都是在 CONSTRUCTOR_TYPE 中定义的。否则,我无法“获取”MethodHandle。但是我收到了一个 WrongMethodTypeException 异常。为什么呢?
编辑:在阅读答案后,以下是代码;现在我不需要中间 map 了:我只需要一个 map,将 String 映射到一个 MethodHandle
@ParametersAreNonnullByDefault
public class PathMatcherProvider
{
    private static final MethodHandles.Lookup LOOKUP
        = MethodHandles.publicLookup();
    private static final MethodType CONSTRUCTOR_TYPE
        = MethodType.methodType(void.class, String.class);

    private final Map<String, MethodHandle> handleMap
        = new HashMap<>();

    public PathMatcherProvider()
    {
        registerPathMatcher("glob", GlobPathMatcher.class);
        registerPathMatcher("regex", RegexPathMatcher.class);
    }

    public final PathMatcher getPathMatcher(final String name, final String arg)
    {
        Objects.requireNonNull(name);
        Objects.requireNonNull(arg);

        final MethodHandle handle = handleMap.get(name);
        if (handle == null)
            throw new UnsupportedOperationException();

        try {
            return (PathMatcher) handle.invokeExact(arg);
        } catch (Throwable throwable) {
            throw new RuntimeException("Unhandled exception", throwable);
        }
    }

    protected final void registerPathMatcher(@Nonnull final String name,
        @Nonnull final Class<? extends PathMatcher> matcherClass)
    {
        Objects.requireNonNull(name);
        Objects.requireNonNull(matcherClass);

        final MethodHandle handle;
        final MethodType type;

        try {
            handle = LOOKUP.findConstructor(matcherClass, CONSTRUCTOR_TYPE);
            type = handle.type().changeReturnType(PathMatcher.class);
            handleMap.put(name, handle.asType(type));
        } catch (NoSuchMethodException | IllegalAccessException e) {
            throw new RuntimeException("cannot find constructor", e);
        }
    }
}
2个回答

18
编译器发出invokeExact调用时,它会将Object记录为期望的返回类型。根据MethodHandle javadoc(强调我的部分):如同虚方法一般,对于invokeExact和invoke的源级调用会编译成一个invokevirtual指令。更不寻常的是,编译器必须记录实际参数类型,并且不能对参数执行方法调用转换。相反,它必须根据它们自己的未转换类型将它们推送到堆栈上。方法句柄对象本身在参数之前被推到堆栈上。然后,编译器使用符号类型描述符调用方法句柄,该符号类型描述符描述了参数和返回类型。为了发布完整的符号类型描述符,编译器还必须确定返回类型。这基于方法调用表达式上的强制转换(如果有),否则如果调用是表达式,则返回Object,否则如果调用是语句,则返回void。在运行时,方法句柄实际返回RegexPathMatcher,因此invokeExact失败并引发WrongMethodTypeException。您需要使用(编译时)强制转换来明确指定返回类型:
return (RegexPathMatcher)handleMap.get(c).invokeExact(arg);

除非您需要对不同的PathMatcher实现进行泛型处理,因此您应该使用asType将方法句柄转换为返回PathMatcher,然后使用PathMatcher作为预期返回类型进行调用。

//in findConstructor
MethodHandle h = LOOKUP.findConstructor(matcherClass, CONSTRUCTOR_TYPE);
return h.asType(h.type().changeReturnType(PathMatcher.class));

//in getPathMatcher
return (PathMatcher)handleMap.get(c).invokeExact(arg);

所以c.cast()的参数类型会被记录为预期的类型?嗯,我完全忽略了这部分,解释得很好;) - Vogel612
1
@Vogel612:不,如果方法调用表达式上没有强制转换,即使在需要更具体类型的地方使用该表达式,预期的返回类型也是Object。签名多态调用不是多重表达式(例如lambda),其中类型在调用上下文中推断。 - Jeffrey Bosboom
好的,我需要深入地再次阅读您的答案,但您建议的代码修改有效... 我会在另一次阅读后接受它 ;) - fge

9

经过3年的发布,我阅读了这篇文章,虽然答案确实正确,但是理解起来相当困难。因此,恕我直言,我将发表一个略有不同的方法(以防像我这样的人必须反复思考才能真正理解)。

主要问题在于两个不同的调用:invokeinvokeExact。但首先,源代码中这两个方法都带有注解

@PolymorphicSignature

也称为编译器重载。这些方法被Java编译器特殊处理——没有其他方法会被同样处理。

为了理解,我们提供一个简单的类,其中只有一个方法:

static class Calle {

    public Object go(Object left, Object right) {
        // do something with left and right
        return new Object();
    }

}

编译并查看生成的字节码长什么样子(javap -c Calle.class)。其中会有这个方法:

public java.lang.Object go(java.lang.Object, java.lang.Object);

它的签名是:两个参数都是java.lang.Object类型,返回值也是java.lang.Object类型。到目前为止,一切都很好。

所以,这样做是完全合法的:

 Calle c = new Calle();
 int left = 3;
 int right = 4;
 c.go(left, right);

而该字节码将如下所示:

invokevirtual #5 // Method CompilerOverloads$Calle.go:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;

该方法接受两个对象,传递两个整数作为参数是完全合法的。

现在考虑方法的定义:

 MethodHandle#invoke

它的签名是java.lang.Object var arg,并返回一个java.lang.Object对象。因此,这段代码将如何编译?
 Lookup l = MethodHandles.lookup();
 MethodType type = MethodType.methodType(Object.class, Object.class, Object.class);
 MethodHandle handle = l.findVirtual(Calle.class, "go", type);
 Object result = handle.invoke(c, left, right); // what is generated here?

有趣的是,它的编译方式与我们的 Calle::go 完全不同。

  Method java/lang/invoke/MethodHandle.invoke:(LCalle;II)Ljava/lang/Object;

它的输入参数是:整数,整数,返回类型是java.lang.Object。就像编译器信任编译时方法声明并根据此生成方法签名一样。

如果我们想将返回类型更改为例如int,我们需要在编译时指定它作为一个强制转换:

 int result = (int) handle.invoke(c, left, right); 

然后它在字节码级别签名更改(重点是我的):

方法 java/lang/invoke/MethodHandle.invoke:(LCalle;II)I

据我所知,在jdk世界中没有其他地方会发生这种情况。

现在,invoke与invokeExact的问题变得有些明显了(一个是精确签名,另一个则稍微宽松一些)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接