2014-12-03 8 views
10

Per uno dei miei progetti devo fare invocazioni dinamiche del costruttore. Ma dal momento che si tratta di Java 7, anziché dell'API di riflessione "classico", utilizzo java.lang.invoke.Perché non posso I .invokeExact() qui, anche se MethodType è OK?

Codice:

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

OK, questo funziona.

Il problema che ho è con questa linea:

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

Se sostituisco invoke con invokeExact, ottengo questo stack trace:

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) 

non ho ben capito. Entrambi GlobPathMatcher e RegexPathMatcher utilizzano un singolo costruttore con un String come argomento e lo MethodType per entrambi è quindi definito in CONSTRUCTOR_TYPE. In caso contrario, non avrei potuto "afferrare" lo MethodHandle s.

Eppure ho un WrongMethodTypeException. Perché?


MODIFICA: ecco il codice dopo aver letto la risposta; ora non ho bisogno la mappa intermedio: ho solo avere una mappa, la mappatura di un String ad un 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); 
     } 
    } 
} 

risposta

12

Quando il compilatore emette la chiamata invokeExact, registra oggetto come tipo di rendimento atteso. Da MethodHandle javadoc (emphasis mine):

Come di consueto con i metodi virtuali, chiamate a livello di origine per invocareExact e richiamare la compilazione su un'istruzione invokevirtual. Più in particolare, il compilatore deve registrare i tipi di argomenti effettivi e non può eseguire conversioni di richiamo del metodo sugli argomenti. Invece, deve spingerli in pila in base ai propri tipi non convertiti. L'oggetto handle del metodo stesso viene inserito nello stack prima degli argomenti. Il compilatore chiama quindi l'handle del metodo con un descrittore di tipo simbolico che descrive l'argomento e i tipi restituiti.

Per emettere un descrittore di tipo simbolico completo, il compilatore deve anche determinare il tipo di reso. Questo è basato su un cast sull'espressione del richiamo del metodo, se ce n'è uno, oppure su Object se il richiamo è un'espressione o non valido se l'invocazione è un'istruzione. Il cast potrebbe essere di tipo primitivo (ma non vuoto).

In fase di esecuzione, il metodo handle restituisce effettivamente RegexPathMatcher, pertanto invokeExact non riesce con WrongMethodTypeException.

è necessario specificare il tipo di ritorno in modo esplicito con un (tempo di compilazione) Cast:

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

Tranne è necessario essere generico su diverse implementazioni PathMatcher, così si dovrebbe convertire il vostro metodo di maniglie per tornare PathMatcher utilizzando asType, quindi chiama con PathMatcher come tipo di ritorno previsto.

//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

quindi il tipo di argomento 'c.cast()' viene registrato come previsto? Hmm ho completamente sovrascritto questa parte, spiegazione meravigliosa;) – Vogel612

+1

@ Vogel612: No, se non c'è cast sull'espressione di chiamata del metodo, il tipo di reso previsto è Object anche se l'espressione viene utilizzata dove è richiesto un tipo più specifico. Le invocazioni polimorfiche di firma non sono espressioni poligonali (ad esempio lambdas) in cui il tipo viene dedotto in contesti di invocazione. –

+0

OK, ho bisogno di rileggere la tua risposta di nuovo in profondità, ma le modifiche al codice che hai suggerito funzionano ... Lo accetterò dopo un'altra lettura;) – fge

2

Dopo 3 anni di distacco, sono venuto a leggere questo e mentre la risposta è effettivamente corretta, era piuttosto difficile da afferrare tutto. Quindi, con tutto il dovuto rispetto, posterò un approccio leggermente diverso (nel caso qualcuno come me dovesse grattarsi la testa due volte a effettivamente capendo).

Il problema principale qui è di due invocazioni diverse: invoke e invokeExact. Ma prima, questi due metodi nel codice sorgente sono annotati con

@PolymorphicSignature

che sono chiamati anche compiler overloads. Questi metodi sono trattati in modo molto speciale dal compilatore java - nessun altro metodo viene trattato nello stesso modo.

Per capire facciamo un esempio. Ecco una semplice classe con un solo metodo:

static class Calle { 

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

} 

compilazione che e guardare a ciò che il bytecode generato assomiglia (javap -c Calle.class). Tra alcune linee ci sarà questo metodo:

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

La firma di esso è: two arguments of type java.lang.Object and a return of type java.lang.Object. Fin qui tutto bene.

Quindi è perfettamente legale per fare questo:

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

E il bytecode che avrà un aspetto:

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

Il metodo accetta due oggetti e due interi sono perfettamente legali per essere passati come parametri.

Ora pensare alla definizione del metodo:

MethodHandle#invoke 

è firma è java.lang.Object var arg e restituisce un java.lang.Object.

Così come verrà compilato questo codice?

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? 

Interessante abbastanza compila molto diversa allora la nostra Calle::go

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

E 'parametri di input sono: Integer, Integer e restituiscono tipo è java.lang.Object. È come se il compilatore avesse fiducia nella dichiarazione del metodo di compilazione e generato la firma del metodo al di fuori di esso.

Se vogliamo cambiare il tipo di ritorno per essere int per esempio, abbiamo bisogno di specificare che come un cast in fase di compilazione:

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

E poi firme cambiamenti a livello di bytecode (enfasi è mio):

metodo Java/lang/richiamare/MethodHandle.invoke: (LCalle; II) I

Questo non accade da nessun'altra parte nel mondo jdk per quanto ne so.

Ed ora il problema di invoke vs invokeExact diventa un po 'ovvio (uno è un esatto firma e l'altro è un po' più sciolto ).

Problemi correlati