15

对于我的一个项目,我必须动态调用构造函数。但由于这是 Java 7,而不是“经典”反射 API,我使用 java.lang.invoke。

代码:

@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));

如果我替换invokeinvokeExact,我会得到这个堆栈跟踪:

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使用一个带有 aString作为参数的构造函数,MethodType因此 for both 是在 中定义的CONSTRUCTOR_TYPE。如果不是这样,我无论如何都无法“抓住” MethodHandles。

然而我得到一个WrongMethodTypeException. 为什么?


编辑:这是我阅读答案后的代码;现在我不需要中间地图:我只需要一张地图,将 a 映射String到 a 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);
        }
    }
}
4

2 回答 2

16

当编译器发出 invokeExact 调用时,它会将 Object 记录为预期的返回类型。来自 MethodHandle javadoc(重点是我的):

与虚拟方法一样,invokeExact 和invoke 的源级调用编译为invokevirtual 指令。更不寻常的是,编译器必须记录实际的参数类型,并且可能不对参数执行方法调用转换。相反,它必须根据它们自己未转换的类型将它们压入堆栈。方法句柄对象本身在参数之前被压入堆栈。然后编译器使用描述参数和返回类型的符号类型描述符调用方法句柄。

要发出完整的符号类型描述符,编译器还必须确定返回类型。这基于对方法调用表达式的强制转换(如果有),或者如果调用是表达式,则为 Object,如果调用是语句,则为 void。强制转换可能是原始类型(但不是 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);
于 2014-12-03T18:43:00.267 回答
8

经过 3 年的发布,我开始阅读这篇文章,虽然答案确实是正确的,但很难掌握所有内容。因此,恕我直言,我将发布一个稍微不同的方法(以防像我这样的人不得不挠头两次才能真正理解)。

这里的主要问题是两个不同的调用 :invokeinvokeExact. 但首先,源代码中的这两个方法都用

@多态签名

也被称为compiler overloads。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);

它的签名是:two arguments of type java.lang.Object and a return of type 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;

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

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

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

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

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

据我所知,这在 jdk 世界的其他任何地方都不会发生。

现在invokevs的问题invokeExact变得有点明显(一个是精确的签名,另一个是更松散的)。

于 2017-03-13T20:21:53.480 回答