2009-08-06 15 views
6

ho un problema non sono in grado di risolvere. Supponiamo abbiamo le seguenti due classi e una relazione di ereditarietà:dinamica bytecode strumentazione - problema

public class A { 
} 

public class B extends A { 
    public void foo() {} 
} 

voglio strumento codice aggiuntivo in modo tale che appare come segue:

public class A { 
    public void print() { } 
} 

public class B extends A { 
    public void foo() { print(); } 
} 

Al fine di raggiungere questo obiettivo, ho basato la mia implementazione sul pacchetto java.lang.instrument, utilizzando un agente con il mio trasformatore di file di classe. Il meccanismo è anche noto come strumentazione bytecode dinamica.

Pezzo di torta finora. Ora, il mio metodo di prova esegue le seguenti operazioni:

Codice:

B b = new B(); 
b.foo(); 

Questo non funziona a causa del seguente restrizione nel pacchetto di strumentazione: quando si chiama new B(), la strumentazione inizia con classe B e finisce in un errore di compilazione quando si carica la classe manipolata mentre la super classe A non ha ancora il metodo print()! Si pone la questione se e come posso far scattare la strumentazione di classe A, prima di classe B. La transform() il metodo della mia classfiletransformer deve essere richiamato con classe A esplicitamente! Così ho iniziato a leggere e urtato questo:

javadoc s' The java.lang.instrument.ClassFileTransformer.transform() dice:

Il trasformatore sarà chiamato per ogni nuova definizione di classe e ogni classe di ridefinizione. La richiesta di una definizione nuova classe è fatto con ClassLoader.defineClass. La richiesta per una ridefinizione di classe è fatto con Instrumentation.redefineClasses o suoi equivalenti nativi.

Il metodo di trasformare arriva con un'istanza di class loader, così ho pensato, perché non chiamare i loadClass metodo (loadClass chiamate defineClass) me stesso con classe A quando è iniziata la strumentazione di B. Mi aspettavo che il metodo dello strumento sarà chiamato come risultato, ma purtroppo questo non era il caso. Invece la classe A è stata caricata senza strumentazione. (L'agente non intercetta il processo di caricamento anche se dovrebbe)

Qualche idea, come risolvere questo problema? Vede un motivo per cui non è possibile che un agente che manipola qualche bytecode non possa caricare manualmente un'altra classe che, si spera, possa inviare anche attraverso quel/qualsiasi agente?

Si noti che il codice seguente funziona correttamente poiché A è stato caricato e strumentato prima che B venga manipolato.

A a = new A(); 
B b = new B(); 
b.foo(); 

Grazie mille!

risposta

8

Non ho riscontrato alcun problema quando ho trasformato B prima di A sul sole 1.6.0 _ 15 e 1.5.0 _ 17 JRE (ho utilizzato ASM). Vorrei ricontrollare il codice di trasformazione eseguendolo esternamente e ispezionando le classi risultanti (ad es. Con javap).Verificherò anche la configurazione del classpath per assicurarmi che A non venga caricato prima del tuo agente per qualche motivo (forse controlla il tuo prefisso con getAllLoadedClasses).


EDIT:

Se si carica classe A nel vostro agente in questo modo:

Class.forName("A"); 

... allora viene generata un'eccezione:

Exception in thread "main" java.lang.NoSuchMethodError: B.print()V 

questo ha un senso - A diventa una dipendenza dell'agent e non avrebbe senso per l'agent allo strumento il suo codice. Otterrai un ciclo infinito che ha comportato un overflow dello stack. Pertanto, A non viene elaborato da ClassFileTransformer.


Per completezza, ecco il mio codice di prova che funziona senza problemi. Come accennato, dipende dalla libreria ASM.

L'agente:

public class ClassModifierAgent implements ClassFileTransformer { 

    public byte[] transform(ClassLoader loader, String className, 
     Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
     byte[] classfileBuffer) throws IllegalClassFormatException { 
    System.out.println("transform: " + className); 
    if ("A".equals(className)) { 
     return new AModifier().modify(classfileBuffer); 
    } 
    if ("B".equals(className)) { 
     return new BModifier().modify(classfileBuffer); 
    } 
    return classfileBuffer; 
    } 

    /** Agent "main" equivalent */ 
    public static void premain(String agentArguments, 
     Instrumentation instrumentation) { 
    instrumentation.addTransformer(new ClassModifierAgent()); 
    } 

} 

Metodo iniettore per A:

public class AModifier extends Modifier { 

    @Override 
    protected ClassVisitor createVisitor(ClassVisitor cv) { 
    return new AVisitor(cv); 
    } 

    private static class AVisitor extends ClassAdapter { 

    public AVisitor(ClassVisitor cv) { super(cv); } 

    @Override 
    public void visitEnd() { 
     MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "print", "()V", 
      null, null); 
     mv.visitCode(); 
     mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", 
      "Ljava/io/PrintStream;"); 
     mv.visitLdcInsn("X"); 
     mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", 
      "println", "(Ljava/lang/String;)V"); 
     mv.visitInsn(Opcodes.RETURN); 
     mv.visitMaxs(2, 1); 
     mv.visitEnd(); 

     super.visitEnd(); 
    } 

    } 

} 

Metodo sostituto per B:

public class BModifier extends Modifier { 

    @Override 
    protected ClassVisitor createVisitor(ClassVisitor cv) { 
    return new BVisitor(cv); 
    } 

    class BVisitor extends ClassAdapter { 

    public BVisitor(ClassVisitor cv) { super(cv); } 

    @Override 
    public MethodVisitor visitMethod(int access, String name, String desc, 
     String signature, String[] exceptions) { 
     if ("foo".equals(name)) { 
     MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "foo", "()V", 
      null, null); 
     mv.visitCode(); 
     mv.visitVarInsn(Opcodes.ALOAD, 0); 
     mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "B", "print", "()V"); 
     mv.visitInsn(Opcodes.RETURN); 
     mv.visitMaxs(1, 1); 
     mv.visitEnd(); 
     return new EmptyVisitor(); 
     } else { 
     return super.visitMethod(access, name, desc, signature, exceptions); 
     } 
    } 
    } 
} 

codice di base comune:

public abstract class Modifier { 

    protected abstract ClassVisitor createVisitor(ClassVisitor cv); 

    public byte[] modify(byte[] data) { 
    ClassReader reader = new ClassReader(data); 
    ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES); 
    ClassVisitor visitor = writer; 
    visitor = new CheckClassAdapter(visitor); 
    visitor = createVisitor(visitor); 
    reader.accept(visitor, 0); 
    return writer.toByteArray(); 
    } 

} 

Per alcuni risultati visibili, ho aggiunto uno System.out.println('X'); a A.print().

quando viene eseguito su questo codice:

public class MainInstrumented { 
    public static void main(String[] args) { 
    new B().foo(); 
    } 
} 

... produce questa uscita:

transform: MainInstrumented 
transform: B 
transform: A 
X 
+0

Thx per la risposta. Vediamo il problema da una prospettiva diversa. Supponiamo che entrambe le classi, A e B, siano vuote. aggiungi un log all'inizio e alla fine del metodo di trasformazione, in modo che possiamo vedere quale classe viene caricata dall'agente e in quale momento. eseguire: new B() il risultato dovrebbe essere: quella classe B viene caricato da l'agente e poi di classe A. si può provare subito a caricare classe A manualmente utilizzando il metodo Classloader.loadClass() in ur agente quando B lo passa? il risultato è: B è stato caricato tramite l'agente, A non lo era! Giusto? Cheers christoph –

+0

Prima di tutto, grazie mille per la risposta e lo sforzo. Lo apprezzo. U hai ragione! Ho usato Javassist per qualsiasi trasformazione. Javassist ricompila le modifiche. Ciò comporta l'errore di compilazione menzionato sopra. ASM lavora direttamente sul bytecode e non c'è bisogno di ricompilare. Riguardo al caricamento di classi all'interno di un agente: non capisco la risposta.Uso eclipse e se aggiungo il mio agente: > // se il nome della classe è B > Class.forName ("A"); e seguo l'esecuzione nel debugger, nessuna eccezione viene lanciata e l'agente non è inserito –

+0

Ancora una volta, hai ragione. L'eccezione che hai citato è corretta. Rendiamo le cose ancora più semplici: entrambe le classi non hanno metodi e non sono pensate per essere strumentate affatto. L'unica cosa che l'agente dovrebbe fare è: se la classe B passa l'agente, Class.forName ("A"); deve essere invocato. Ciò dovrebbe attivare l'ordine corretto di caricamento della classe (prima A, poi B). Prova questo esempio. Vedrai che solo B passa l'agente! Quindi sorge la domanda perché A non passa l'agente quando viene chiamato come parte di un agente. –