2014-12-03 3 views
10

Для одного из моих проектов мне нужно сделать динамические вызовы конструктора. Но поскольку это Java 7, вместо «классического» API отражения я использую java.lang.invoke.Почему я не могу .invokeExact() здесь, хотя MethodType в порядке?

Код:

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


EDIT: вот код после того, как я прочитал ответ; Теперь мне не нужно промежуточной карты: Я просто должны иметь одну карту, отображение а 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); 
     } 
    } 
} 

ответ

12

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

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

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

Во время выполнения дескриптор метода фактически возвращает 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); 
+0

так что тип аргументов 'c.cast()' записывается как ожидаемый тип? Хм, я полностью перепробовал эту часть, замечательное объяснение;) – Vogel612

+1

@ Vogel612: Нет, если в выражении вызова метода нет, ожидаемый тип возвращаемого значения - Object, даже если выражение используется там, где требуется более конкретный тип. Полиморфные вызовы подписи не являются поли-выражениями (например, lambdas), где тип выводится в контекстах вызова. –

+0

ОК, мне нужно снова прочитать ваш ответ по-разному, но предложенные вами модификации кода ... Я буду принимать его после другого чтения;) – fge

2

После 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/языки/Объект; Ljava/языки/объект;) Ljava/языки/объект;

Метод принимает два объекта, а два целых числа вполне законны для передачи в качестве параметров.

Теперь подумайте об определении метода:

MethodHandle#invoke 

это подпись java.lang.Object вар аргумент и возвращает 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/языки/вызова/MethodHandle.invoke: (LCalle; II) I

Этого не происходит нигде в мире jdk, насколько я знаю.

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