Nicht die Bohne kompliziert: Java Bytecode-Engineering

Tobias Nebel Dieser Artikel wurde von geschrieben. Tobias Nebel hat sein Studium der Telekommunikationsinformatik an der Hochschule für Telekommunikation in Leipzig absolviert. Er arbeitet als Softwareentwickler bei der Firma ubigrate mit dem Schwerpunkt Geräteintegration.

Analyse, Generierung oder auch Manipulation von Java-Programmen zur Laufzeit sind nützliche Technologien in der Java-Softwareentwicklung. Durch diese Funktionen können beispielsweise Ansätze wie die Aspekt-orientierte Programmierung (AOP) werden. Dabei können diese Funktionalitäten sowohl am Quellcode, als auch an bereits compiliertem Bytecode realisiert werden. Der Vorteil der Bytecode-Manipulation besteht darin, dass kein erneutes Compilieren des Quellcodes notwendig ist. Zu genau diesem Zweck existieren verschiedene Bibliotheken mit jeweils verschiedenen Ansätzen und Zielen. Die wohl bekanntesten Bibiotheken sind ASM von ObjectWeb, BCEL von der Apache Software Foundation und Javassist als Unterprojekt von JBoss. In diesem Beitrag sollen Eigenschaften dieser Bibliotheken kurz beleuchtet werden.

ASM

ASM behauptet von sich selbst, die schnellste aller Bibliotheken zur Bytecode-Manipulation zu sein. Mittels des Visitor-Patterns kann hier der ByteCode des .class-Files durchwandert und gegebenenfalls manipuliert werden. Das folgende Codebeispiel übergibt der Klasse ClassReader ein InputStream-Objekt des zu parsenden .class-Files. Über die accept(…)-Methode wird ein Objekt übergeben, welches das ClassVisitor-Interface implementiert. In diesem Beispiel generiert der ClassWriter den Bytecode aus der Folge von gerufenen visitXYZ(…)-Methoden.

ClassReader cr = new ClassReader(
new FileInputStream( pathToClassFile ) );
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cr.accept(cw, ClassReader.SKIP_DEBUG);
cw.toByteArray();   // Byte-Array der Java-Klasse

Durch Erweitern der Klasse ClassAdapter können mehrere ClassVisitor-Objekte verkettet werden, um beispielsweise verschiedene Manipulationen in extra Klassen auszulagern:

ClassReader cr = new ClassReader(
          new FileInputStream( pathToClassFile ) );
ClassWriter cw = new ClassWriter( ClassWriter.COMPUTE_MAXS );
ClassVisitor mtv =
          new MyMethodTransformerClassVisitor( cw );
ClassVisitor atv = 
          new MyAnnotationTransformerClassVisitor( mtv );
cr.accept( atv, ClassReader.EXPAND_FRAMES );

In diesem Beispiel werden die visitXYZ()-Methoden der Klassen MyAnnotationTransformerClassVisitor, MyMethodTransformerClassVisitor und ClassWriter in dieser Reihenfolge durch die Instanz der ClassReader-Klasse aufgerufen. Dabei können die Implementierungen der Transformer-Klassen die gewünschten Manipulationen durchführen. Ein Objekt der Klasse ClassNode kann auch eine Klasse repräsentieren:

ClassReader cr = new ClassReader(
new FileInputStream(pathToClassFile ) );
ClassNode cn = new ClassNode();
cr.accept(cn, true);

ASM punktet mit der sehr guten Dokumentation, die neben ausführlichen Erklärungen auch nützliche Codebeispiele enthält.

Vorteile:

  • schnell
  • klein
  • gute Dokumentation

Nachteile:

  • geringe Abstraktion des Bytecodes

BCEL

Mit BCEL wird der Bytecode einer Klasse in Objekte geladen, die diesen repräsentieren. Beispielsweise kann ein .class-File in ein JavaClass-Object geparst werden. Eigenschaften der Java-Klasse können so mittels dieser Objekte nun über get- und set-Methoden abgerufen bzw. manipuliert werden. Ebenso kann ein neues JavaClass-Objekt mit gewünschten Eigenschaften, wie beispielsweise Methoden, erzeugt werden. Dazu dient die Klasse ClassGen, die sowohl leer erzeugt werden kann, als auch mit einem existierenden JavaClass-Objektes initialisiert werden kann. Zur Abstraktion gibt es dabei wiederum Objekte, welche diese Bestandteile repräsentieren und den Zugriff darauf übernehmen. Der Programmierer kann also sequentiell gezielt Eigenschaften eines .class-Files manipulieren. Die den Bytecode repräsentierenden Objekte sind hierarchisch aufgebaut: Beispielsweise wird die Anweisung iinc zum Inkrementieren eines Integer-Wertes durch die Unterklasse org.apache.bcel.generic.IINC der Klasse LocalVariableInstruction dargestellt, wobei letztere wiederum Unterklasse der Klasse Instruction ist. Dieser Aufbau gewährleistet eine intuitive Benutzung der BCEL-Bibliothek.

InstructionList il = new InstructionList(); 
ClassGen  cg = new ClassGen("HelloWorld",
"java.lang.Object",
"<generated>",
ACC_PUBLIC | ACC_SUPER, null);
MethodGen mg = new MethodGen(ACC_STATIC | ACC_PUBLIC,
Type.VOID,
new Type[] { new ArrayType(Type.STRING, 1) },
new String[] { "argv" },
"main",
"HelloWorld", il, cp);
...
cg.addMethod( mg.getMethod() );
il.dispose();
cg.getJavaClass().getBytes(); // Byte-Array der Java-Klasse

Als alternative Strategie zur Abarbeitung der Inhalte eines .class-Files existiert ähnlich wie bei ASM eine Implementierung des Visitor-Patterns. Dabei kann auch hier vom hohen Abstraktionsgrad profitiert werden, da die Visitor-Implementierung entsprechend der Klassenhierarchie aufgebaut ist. So kann also der jeweiligen Klasse ein eigenes Visitor-Objekt übergeben werden, welches dann die entsprechende visitINSTRUCTION-Methode implementiert: Die iinc Anweisung wird demnach mit jeder der Funktionen visitIINC(…), visitLocalVariableInstruction(…), und visitInstruction(…) besucht. Die recht hohe Abstraktion des Bytecodes hat letztlich aber auch negative Seiten: dazu gehört die Größe der Bibliothek (reichliche 500kB) sowie die verhältnismäßig langsame Abarbeitung. Genauso ärgerlich ist, dass in Version 5.2 noch keine Annotations unterstützt werden. Mit der geplanten Version 5.3 soll aber Abhilfe geschaffen werden.

Vorteile:

  • schneller Einstieg
  • hohe Abstraktion des entsprechenden Bytecodes

Nachteile:

  • langsam
  • große Bibliothek
  • keine Annotations in v5.2 (v5.3 ist in Entwicklung und unterstützt diese)

Javassist

Javassist ist ein Unterprojekt von JBoss. Es ähnelt BCEL insofern, dass der Bytecode ebenfalls durch entsprechend erzeugte Objekte repräsentiert wird. Die Abarbeitung geschieht ähnlich schnell. Die Manipulation einer Klasse geschieht dabei über die Klasse CtClass. Eine Instanz dieser Klasse zur Manipulation eines .class-Files muss über die Klasse ClassPool beschafft werden:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");

oder

CtClass cc = pool.makeClass(
new FileInputStream( pathToClassFile ) );

Javassist verfügt zudem über einen eigenen Compiler, mit dem auch das Einfügen übersetzter Java-Snippets in .class-Files möglich ist. Das Hinzufügen neuer Methoden kann demnach so geschehen:

CtMethod m = CtNewMethod.make(
              "public int add(int x, int y)
              { return x+y; }"
              , cc);
cc.addMethod(m); 

Vorteile:

  • hoher Abstraktionsgrad
  • Methoden-Erzeugung dank eigenem Compiler auch aus Java-Snippets

Nachteile:

  • verhältnismäßig langsam
  • große Bibliothek

Geschwindigkeitsvergleich

Zum Testen der Geschwindigkeit der Bibliotheken wurden verschiedene .class-Files eingelesen und neu geschrieben. Für einen guten Durchschnitt wurde jedes .class-File 1000mal prozessiert. Die Ergebnisse sind in der Grafik ersichtlich. Javassist und BCEL liegen sehr nah beieinander, was dem hohen Abstraktionsgrad des Bytecodes geschuldet ist. Dagegen ist ASM fast doppelt so schnell in der Abarbeitung.

Post to Twitter

Schlagworte:

1 Kommentar bisher »

  1. Metadaten, oder: Warum man Volt und milliVolt nicht einfach addieren kann. | universal.adapter sagt

    am 8. Dezember 2008 @ 22:15

    [...] wie kann man mit Java soetwas erreichen? Das Zauberwort heißt Bytecode-Engineering. Prinzipielles Ziel ist, dass der Programmierer weiterhin mit primitiven Datentypen wie z.B. int, [...]

Komentar RSS · TrackBack URI

Hinterlasse einen Kommentar

Name:

eMail:

Website:

Kommentar:

 

google

google

asus