Почему я не могу.invokeExact() здесь, хотя MethodType в порядке?

Для одного из моих проектов я должен сделать динамические вызовы конструктора. Но так как это 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));

Если я заменю invoke с invokeExactЯ получаю эту трассировку стека:

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, Зачем?


РЕДАКТИРОВАТЬ: вот код после того, как я прочитал ответ; теперь мне не нужна промежуточная карта: мне просто нужно иметь одну карту, отображающую 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 ответа

Решение

Когда компилятор отправляет вызов invokeExact, он записывает Object в качестве ожидаемого возвращаемого типа. Из MethodHandle Javadoc (выделение мое):

Как обычно в виртуальных методах, вызовы на уровне исходного кода для invokeExact и вызова compile для инструкции invokevirtual. Еще более необычно, что компилятор должен записывать фактические типы аргументов и может не выполнять преобразования вызовов методов для аргументов. Вместо этого он должен поместить их в стек в соответствии с их собственными необращенными типами. Сам объект-дескриптор метода помещается в стек перед аргументами. Затем компилятор вызывает дескриптор метода с дескриптором символьного типа, который описывает аргумент и возвращаемый тип.

Чтобы выпустить полный дескриптор символьного типа, компилятор также должен определить тип возвращаемого значения. Это основано на приведении к выражению вызова метода, если оно есть, или Object, если вызов является выражением, или void, если вызов является оператором. Приведение может быть примитивным типом (но не пустым).

Во время выполнения дескриптор метода фактически возвращает RegexPathMatcher, поэтому invokeExact завершается с ошибкой WrongMethodTypeException.

Вам необходимо явно указать тип возвращаемого значения с помощью приведения (во время компиляции):

return (RegexPathMatcher)handleMap.get(c).invokeExact(arg);

За исключением того, что вам нужно быть универсальным для разных реализаций PathMatcher, поэтому вы должны преобразовать дескрипторы своих методов, чтобы они возвращали PathMatcher, используя asType, а затем вызывать с 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);

После 3-х лет публикации я пришел, чтобы прочитать это, и хотя ответ действительно правильный, все было довольно трудно понять. Поэтому, при всем моем уважении, я опубликую немного другой подход (на тот случай, если кому-то вроде меня придется дважды почесать голову, чтобы понять).

Основная проблема здесь - два разных вызова: invoke а также invokeExact, Но сначала эти два метода в исходном коде помечены

@PolymorphicSignature

которые называются 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 // Метод 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.

А теперь проблема invoke против invokeExact становится немного очевидным (одна является точной подписью, а другая немного более свободной).

Другие вопросы по тегам