2009-08-06 8 views
6

Ich habe ein Problem, das ich nicht lösen kann. Nehmen wir an, wir die folgenden zwei Klassen und eine Vererbungsbeziehung haben:Dynamische Bytecode-Instrumentierung - Problem

public class A { 
} 

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

I-Code zusätzliches Instrument wollen, so dass es wie folgt aussieht:

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

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

Um dieses Ziel zu erreichen, basierend ich meine Implementierung auf dem java.lang.instrument Paket, mit einem Agenten mit meiner eigenen Klassendatei Transformator. Der Mechanismus wird auch als dynamische Bytecode-Instrumentierung bezeichnet.

Stück Kuchen so weit. Nun, meine Testmethode macht folgender:

Code:

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

Dies nicht wegen der folgenden Einschränkung funktioniert in der Instrumentierung Paket: wenn new B() Aufruf, die Instrumentierung beginnt mit der Klasse B und enden in einem Kompilierungsfehler beim Laden der manipulierten Klasse, da die Superklasse A noch keine print() -Methode hat! Die Frage stellt sich, ob und wie ich die Instrumentierung der Klasse A vor Klasse B auslösen kann. Die transform() - Methode meines classfiletransformers sollte explizit mit der Klasse A aufgerufen werden! Also begann ich zu lesen und stieß auf diese:

Die java.lang.instrument.ClassFileTransformer.transform() ‚s javadoc sagt:

Der Transformator für jede neue Klassendefinition und jede Klasse Neudefinition genannt wird. Die Anforderung für eine neue Klassendefinition erfolgt mit ClassLoader.defineClass. Die Anforderung für eine Klassenredefinition wird mit Instrumentation.redefineClasses oder seinen nativen Entsprechungen gestellt.

Das Transformationsverfahren zusammen mit einer Class-Loader-Instanz kommt, also dachte ich, warum nicht die loadClass Methode (loadClass Anrufe defineClass) rief mich mit Klasse A, wenn die Instrumentierung von B begonnen hat. Ich habe erwartet, dass die Instrument-Methode als Ergebnis aufgerufen wird, aber das war leider nicht der Fall. Stattdessen wurde die Klasse A ohne Instrumentierung geladen. (Der Agent fängt den Ladevorgang nicht ab, obwohl er es soll)

Irgendwelche Ideen, wie man dieses Problem löst? Sehen Sie einen Grund, warum es nicht möglich ist, dass ein Agent, der einen Bytecode manipuliert, nicht manuell eine andere Klasse laden kann, die dann hoffentlich auch über diesen/irgendeinen Agenten gesendet wird?

Beachten Sie, dass der folgende Code ordnungsgemäß funktioniert, da A geladen und instrumentiert wurde, bevor B manipuliert wird.

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

Vielen Dank!

Antwort

8

Ich habe keine Probleme sehen, wenn ich B umgewandelt, bevor A auf der Sun 1.6.0 _ 15 und 1.5.0 _ 17 JREs (I verwendet ASM). Ich würde den Transformationscode überprüfen, indem ich ihn extern ausführe und die resultierenden Klassen (z. B. mit javap) inspiziere.Ich würde auch Ihre Classpath-Konfiguration überprüfen, um sicherzustellen, dass A aus irgendeinem Grund nicht vor Ihrem Agenten geladen ist (überprüfen Sie vielleicht Ihre Premium-Adresse mit getAllLoadedClasses).


EDIT:

Wenn Sie Klasse A in Ihrem Agenten wie folgt laden:

Class.forName("A"); 

... dann wird eine Ausnahme ausgelöst:

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

Dies macht Sinn - A wird eine Abhängigkeit des Agenten und es wäre nicht sinnvoll für den Agenten zu instrumentieren sein eigener Code. Sie erhalten eine Endlosschleife, die zu einem Stapelüberlauf führte. Daher wird A nicht von ClassFileTransformer verarbeitet.


Der Vollständigkeit halber ist hier mein Testcode, der ohne Problem funktioniert. Wie erwähnt, kommt es auf die ASM-Bibliothek an.

AGENTEN:

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()); 
    } 

} 

Injektors Verfahren zur 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(); 
    } 

    } 

} 

Method replacer für 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); 
     } 
    } 
    } 
} 

gemeinsamen Basiscode:

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(); 
    } 

} 

Für einige sichtbare Ergebnisse fügte ich System.out.println('X'); zu A.print() hinzu.

Wenn auf diesem Code ausführen:

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

... es erzeugt diese Ausgabe:

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

Thx für deine Antwort. Lassen Sie uns das Thema aus einer anderen Perspektive betrachten. Angenommen, beide Klassen A und B sind leer. Fügen Sie am Anfang und am Ende der Transformationsmethode ein Protokoll hinzu, damit wir sehen können, welche Klasse zu welchem ​​Zeitpunkt vom Agenten geladen wird. ausführen: new B() das Ergebnis sollte sein: die Klasse B wird vom Agenten geladen und anschließend Klasse A. können Sie jetzt versuchen Klasse A zu laden manuell die Classloader.loadClass() -Methode in ur Mittel verwendet wird wenn B es passiert? das Ergebnis ist: B wurde durch den Agenten geladen, A war nicht! Richtig? Cheers christoph –

+0

Zunächst einmal vielen Dank für die Antwort und die Mühe. Ich weis das zu schätzen. U sind richtig! Ich habe Javassist für alle Transformationen verwendet. Javassist kompiliert die Änderungen neu. Dies führt zu dem oben erwähnten Kompilierungsfehler. ASM arbeitet direkt am Bytecode und es ist keine Neukompilierung erforderlich. Zum Laden von Klassen innerhalb eines Agenten: Ich verstehe Ihre Antwort nicht.Ich benutze Eclipse und wenn ich in meinem Agenten hinzufügen: > // wenn Klassenname ist B > Class.forName ("A"); und ich folge der Ausführung im Debugger, keine Ausnahme wird geworfen und der Agent wird nicht eingegeben –

+0

Wieder haben Sie Recht. Die von Ihnen erwähnte Ausnahme ist korrekt. Lassen Sie uns die Dinge noch einfacher machen: Beide Klassen haben keine Methoden und sollen überhaupt nicht instrumentiert werden. Das einzige, was der Agent tun sollte, ist: wenn Klasse B den Agenten übergibt, Class.forName ("A"); soll aufgerufen werden. Dies sollte die richtige Reihenfolge des Klassenladens auslösen (A zuerst, dann B). Probieren Sie dieses Beispiel. Du wirst sehen, dass nur B den Agenten passiert! Daher stellt sich die Frage, warum A den Agenten nicht weitergibt, wenn er als Teil eines Agenten aufgerufen wird. –