2016-07-12 40 views
0

Ich schreibe eine Anwendung, in der reflektierte Methodenobjekte mit spezifischen Signaturen in regulären INVOKEVIRTUAL-Aufrufen in durch ASM generierten Klassen ausgepackt werden, so dass diese Methoden sein können wiederholt leistungsorientiert aufgerufen. Methoden, die ausgepackt werden sollen, haben immer einen spezifischen Rückgabetyp und einen ersten Parameter, können aber eine beliebige Anzahl anderer Parameter eines beliebigen Typs nach diesem Punkt haben.Alle Reflektionsmethoden, die auf den Konstruktor der durch ASM generierten Klasse zugreifen, werfen NoClassDefFoundError, wenn die Klasse primitive Typen referenziert

Ich habe zwei Klassen definiert, um dies zu tun, InvokerProxy und NewInvokerProxyFactory.

public interface InvokerProxy { 
    ExitCode execute(IODescriptor io, Object... args); 
} 

public final class NewInvokerProxyFactory { 

    private static final String GENERATED_CLASS_NAME = "InvokerProxy"; 

    private static final Map<Class<?>, Consumer<MethodVisitor>> UNBOXING_ACTIONS; 

    private static final AtomicInteger NEXT_ID = new AtomicInteger(); 

    private NewInvokerProxyFactory() {} 

    public static InvokerProxy makeProxy(Method backingMethod, Object methodParent) { 
     String proxyCanonicalName = makeUniqueName(InvokerProxyFactory.class.getPackage(), backingMethod); 
     String proxyJvmName = proxyCanonicalName.replace(".", "/"); 

     ClassWriter cw = new ClassWriter(0); 
     FieldVisitor fv; 
     MethodVisitor mv; 

     cw.visit(V1_8, ACC_PUBLIC | ACC_SUPER, proxyJvmName, null, Type.getInternalName(Object.class), new String[]{Type.getInternalName(InvokerProxy.class)}); 

     cw.visitSource("<dynamic>", null); 

     { 
      fv = cw.visitField(ACC_PRIVATE + ACC_FINAL, "parent", Type.getDescriptor(Object.class), null, null); 
      fv.visitEnd(); 
     } 

     { 
      mv = cw.visitMethod(ACC_PUBLIC, "<init>", Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(Object.class)), null, null); 
      mv.visitCode(); 
      mv.visitVarInsn(ALOAD, 0); 
      mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(Object.class), "<init>", "()V", false); 
      mv.visitVarInsn(ALOAD, 0); 
      mv.visitVarInsn(ALOAD, 1); 
      mv.visitFieldInsn(PUTFIELD, proxyJvmName, "parent", Type.getDescriptor(Object.class)); 
      mv.visitInsn(RETURN); 
      mv.visitMaxs(2, 2); 
      mv.visitEnd(); 
     } 

     { 
      mv = cw.visitMethod(ACC_PUBLIC + ACC_VARARGS, "execute", Type.getMethodDescriptor(Type.getType(ExitCode.class), Type.getType(IODescriptor.class), Type.getType(Object[].class)), null, null); 
      mv.visitCode(); 

      mv.visitVarInsn(ALOAD, 0); 
      mv.visitFieldInsn(GETFIELD, proxyJvmName, "parent", Type.getDescriptor(Object.class)); 
      mv.visitTypeInsn(CHECKCAST, Type.getInternalName(methodParent.getClass())); 
      mv.visitVarInsn(ALOAD, 1); 

      Class<?>[] paramTypes = backingMethod.getParameterTypes(); 
      for (int i = 1; i < paramTypes.length; i++) { 
       mv.visitVarInsn(ALOAD, 2); 
       mv.visitLdcInsn(i-1); 
       mv.visitInsn(AALOAD); 
       mv.visitTypeInsn(CHECKCAST, Type.getInternalName(paramTypes[i])); 
       if (paramTypes[i].isPrimitive()) { 
        UNBOXING_ACTIONS.get(paramTypes[i]).accept(mv); 
       } 
      } 

      mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(methodParent.getClass()), backingMethod.getName(), Type.getMethodDescriptor(backingMethod), false); 
      mv.visitInsn(ARETURN); 
      mv.visitMaxs(backingMethod.getParameterTypes().length + 2, 3); 
      mv.visitEnd(); 
     } 
     cw.visitEnd(); 

     try { 
      return (InvokerProxy) SystemClassLoader.defineClass(proxyCanonicalName, cw.toByteArray()).getDeclaredConstructor(Object.class).newInstance(methodParent); 
     } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { 
      throw new InvokerProxyGenerationException("Exception creating invoker proxy for method '" + backingMethod + "'", e); 
     } 
    } 

    private static String makeUniqueName(Package parentPackage, Method method) { 
     return String.format("%s.%s_%d", parentPackage.getName(), GENERATED_CLASS_NAME, NEXT_ID.getAndIncrement()); 
    } 

    static { 
     Map<Class<?>, Consumer<MethodVisitor>> actions = new HashMap<>(); 
     actions.put(Byte.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Byte.class), "byteValue", "()B", false)); 
     actions.put(Short.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Short.class), "shortValue", "()S", false)); 
     actions.put(Integer.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Integer.class), "intValue", "()I", false)); 
     actions.put(Long.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Long.class), "longValue", "()J", false)); 
     actions.put(Float.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Float.class), "floatValue", "()F", false)); 
     actions.put(Double.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Double.class), "doubleValue", "()D", false)); 
     actions.put(Boolean.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Boolean.class), "booleanValue", "()Z", false)); 
     actions.put(Character.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Character.class), "charValue", "()C", false)); 
     UNBOXING_ACTIONS = actions; 
    } 
} 

Durch Tests habe ich entdeckt, dass, wenn das Verfahren durch den InvokerProxyFactory keine primitive Parameter hat ausgepackt werden (int, char, float, etc ..), versucht, einen Konstruktor zu sehen Für diese Klasse durch eine der normalerweise zur Verfügung gestellten Reflexionsverfahren (Class.getConstructors, Class.getDeclaredConstructor, etc ...) wird eine java.lang.NoClassDefFoundError führen, die den ersten in der Methodensignatur gefundenen primitiven Typ als ihre Nachricht zitiert. Die Ausnahme wird anscheinend von URLClassLoader.findClass verursacht, wo ein ClassNotFoundException mit der gleichen Nachricht ausgelöst wird.

Scheinbar dieses Problem geht sogar über Konstruktoren hinaus, da sogar Unsafe.allocateInstance diese gleiche Ausnahme beim Erstellen einer Instanz der generierten Klasse auslöst. Es gibt auch absolut keine Probleme, Konstruktoren nachzuschlagen oder Instanzen zu erstellen, wenn die unverpackte Methode keine primitiven Parameter hat.

+0

Können Sie die Stapelspur der Ausnahmebedingung und die generierte Klassendatei nach Möglichkeit veröffentlichen? Gibt es auch einen Grund, warum Sie nicht nur aufgerufenenDynamic verwenden können? Es wurde bereits entwickelt, um das zu tun, was Sie tun, aber effizienter. – Antimony

+0

Interessantes Projekt, aber ich bin mir ziemlich sicher, dass die Reflection-Implementierung und der Hotspot-Compiler dies bereits tun. Ich würde Interesse an Benchmarks haben, sobald Sie das laufen haben. –

+0

@ Jörn Horstmann: Ich bin sehr zuversichtlich, dass ein direkter 'MethodHandle' die Boxen nicht braucht varargs ist möglicherweise effiziente oder zumindest auf dem Niveau diesen im Vergleich zu. Und wenn die Anzahl der Parameter fest ist, kann 'LambdaMetaFactory' dasselbe tun. – Holger

Antwort

1

Der folgende Code sieht sehr verdächtig

mv.visitTypeInsn(CHECKCAST, Type.getInternalName(paramTypes[i])); 

Dieser Code bedingungslos genannt wird, selbst wenn paramTypes[i] ein primitiver Typ ist. Die ASM documentation besagt jedoch, dass getInternalName nur für ein reales Objekt oder Array-Typ aufgerufen werden kann. ASM erzeugt wahrscheinlich nur einen falschen Klassennamen, wenn ihm ein Primitiv zugewiesen wird, daher der Fehler.

public static String getInternalName(Class c) 

Liefert die internen Namen der gegebenen Klasse. Der interne Name einer Klasse ist ihr vollständiger qualifizierter Name, wie von Class.getName() zurückgegeben, wobei '.' werden ersetzt durch durch '/'.

Parameter:

c - ein Objekt oder Array-Klasse.

Rückgabe:

der interne Name der angegebenen Klasse.

Beachten Sie auch, dass die Anweisung CHECKCAST sowieso nicht für primitive Typen gültig ist.

+0

Das Ergebnis ist ziemlich vorhersehbar. 'Class.getName()' gibt den Namen des Grundtyps zurück, z. 'int', Ersetzen von' .' durch '/' hat keine Wirkung, und die Verwendung dieses Namens als interner Name bewirkt, dass die JVM nach einer Klasse mit diesem Namen sucht, z. eine Klasse mit dem Namen "int" (es sei denn, der Verifizierer weist sie vorher zurück, weil ein Referenztyp "int" nicht an eine Methode übergeben werden kann, die den primitiven Typ "int" erwartet - was zuerst geprüft wird, ist implementierungsspezifisch). – Holger

+0

@holger Das dachte ich mir, denn das wäre die naivste Implementierung, aber ich wollte keine Annahmen treffen, ohne die Implementierungsquelle zu betrachten. – Antimony

+0

Nun, so beschreibt es auch die Dokumentation. Da kein anderes Verhalten erwähnt wird, gibt es keinen Grund, ein besonderes Verhalten anzunehmen (das einzige anzunehmende spezielle Verhalten wäre das Auslösen einer Ausnahme, was offensichtlich hier nicht vorkam). – Holger