Einfache Java-Bytecode-Instrumentalisierung

In modernen Architekturen ist der Einsatz von Frameworks und 3rd Party Libraries selbstverständlich. Erfolgt die Entwicklung in einem Unternehmen, so sind die Zuständigkeiten für das Gesamtsystem häufig unter anderem zwischen den Teams der technischen Architektur und der Entwicklung verteilt. Besteht das Bedürfnis seitens des Teams Entwicklung, zu Analysezwecken in die Frameworks und Libraries programmatisch tiefer einzugreifen, als von der Architektur vorgesehen, sind solche Änderungen in der Regel mit extra Aufwand seitens Team Architektur verbunden. Neben dem zusätzlichen Aufwand spielen die Sicherheit und die Qualität des Frameworks eine wichtige Rolle – Änderungen im Framework, die nur eine ganz spezielle und unter Umständen einmalig auftretende Frage beantworten, aber nicht in der Produktion wirksam sein sollen, sind zu vermeiden.

Wie kann so eine Frage im Kontext einer komplexen Applikation aussehen? Eines der Architektur-Objekte, das in Client-Server-GUI-Applikationen denkbar ist, ist der Environment Context. Die konkrete Fragestellung könnte sein „Welche Daten werden dem Kontext zur Laufzeit übergeben? Wer übergibt die Daten?“

Sicherlich, eine ganz spezielle Situation. Doch keine seltene. Eine mögliche Lösung wäre, das Logging (z.B. Log4J) zu verwenden, um die nötige Information festzuhalten. Nachteilig dabei ist, dass man tatsächlich den Sourcecode modifizieren muss. Befinden sich die zu untersuchenden Methoden in einer Library oder einem Framework, deren Sourcecode nicht modifiziert werden soll – zum Beispiel weil kein Zugriff auf den Framework-Sourcecode vorhanden ist oder der Build zu aufwendig wäre oder die Änderung sowieso nicht live geht, sondern vor oder während der Implementierungsphase „nur“ der besseren Analyse dienen soll – kann Java Instrumentation die nötigen Werkzeuge zur Verfügung stellen.

Was ist Java Instrumentation?

Das Java Instrumentation Package (java.lang.instrument) ermöglicht das Bytecode-Anreichern von kompilierten Java-Programmen. Das Anreichern wird von Java Agents übernommen, die zusammen mit dem eigentlichen Programm in einer JVM (Java Virtual Machine) laufen. Java Agent kann das Programm beeinflussen, indem der Bytecode der Klassen, die das Programm ausmachen, zur Ladezeit durch den Java Agent verändert werden kann. Das erlaubt den Java Agents, den Klassenladeprozess zu kontrollieren und zusätzliche Logik in Methoden und Klassen zu platzieren. Diese zusätzliche Logik kann zum Beispiel Tracing-Informationen loggen oder Callback-Aufrufe zur Agent Library absetzen. Das Verhalten des Programms kann also nicht nur durch das Verändern von Sourcecode beeinflusst werden, sondern „indirekt“, indem bereits kompilierter Sourcecode modifiziert wird.

Ist der Java Agent in die Datei agent.jar gepackt, erfolgt der Aufruf der Anwendung „Application“ zusammen mit dem Agent wie folgt:

Im Classpath sieht man den Verweis zu dem Tool Javassist. Javassist ermöglicht die Änderungen am Bytecode einer Klasse zum Zeitpunkt des Ladens der Klasse. Im Vergleich zu anderen, ähnlichen Tools bietet Javassist die Option an, neue Logik in der Form von Sourcecode anzugeben. Eine andere Möglichkeit wäre, direkt mit den Bytecode-Instruktionen zu arbeiten – dafür muss man die JVM-Bytecode-Spezifikation genau verstehen. Javassist nimmt den als Zeichenkette (java.lang.String) angegebenen Sourcecode, übersetzt diesen in den Bytecode und fügt an der angegebenen Stelle in der zu modifizierenden Klasse ein.

Beispiel

Als einfaches Beispiel für die Anwendung von Java Instrumentation nehmen wir eine ebenfalls simple Applikation, die zwei Zahlen zurückgeben und diese summieren kann:

Führt man die Anwendung aus, kommt als Ausgabe „1 + 3 = 4“. Nun wollen wir wissen, wann und von wem die Methode „sum“ aufgerufen wird, und was die Werte der übergebenen Parameter sind. Das sind unsere Anforderungen an den Java Agent. Eine Java-Agent-Klasse implementiert ClassFileTransformer aus dem java.lang.instrument-Package und enthält die Methode „premain()“.

Die premain-Methode wird noch vor der main-Methode aufgerufen. Dort wird unser Agent instanziiert und vor der eigentlichen Applikation statisch geladen.

Die transform-Methode kommt aus dem ClassFileTransformer-Interface. Dort kann die geladene Klasse als byte[]-Array modifiziert und weiter an die JVM übergeben werden. Genau das macht unsere transform-Methode. Java-eigenen Klassen sollen unverändert bleiben, deswegen werden Klassen, deren Package mit „java“ anfängt, einfach zurückgegeben. Alle anderen Klassen werden an unsere Bedürfnisse angepasst zurückgegeben. Das bereits erwähnte Tool Javassist ermöglicht ein komfortables Modifizieren des Bytecodes.

In der Methode transformClass, die hier der Übersichtlichkeit halber ohne Fehlerbehandlung angegeben ist, werden die Methoden der geladenen Klasse nacheinander betrachtet und ggf. geändert:   

Nun kommen wir zur eigentlichen Implementierung der Anforderungen. In transformMethod wird dem Tool Javassist mit insertBefore und insertAfter Sourcecode zum Kompilieren und Einfügen übergeben. Mit dem eingefügten Quellcode sehen wir bei jeder ausgeführten Methode, wenn diese betreten und wieder verlassen wird.

Um spezielle Anforderung an die sum-Methode umzusetzen, wird der „Enter-Leave“-Sourcecode noch um weitere Logik ergänzt. In sumBefore wird der Wert des ersten und des zweiten Parameters ausgegeben, in sumAfter die aufrufende Klasse aus dem Stacktrace ermittelt.

Folgendermaßen sieht die Ausgabe auf der Konsole nach dem Ausführen des Programms mit unserem Agent aus:

Wir sehen, wie Klassen- und Objektinitialisierungsmethoden aufgerufen werden, wie die main-Methode aufgerufen wird, wie die Getter für die Operanden aufgerufen und wieder verlassen werden. Es ist zu sehen, dass die sum-Methode aufgerufen wird. Die Parameter und der Aufrufer werden ausgegeben. Danach wird die Methode verlassen, und in der main-Methode erfolgt die Ausgabe „1 + 3 = 4“.

Fazit

Bei speziellen Anforderungen, bei denen bestehende Funktionalität ohne Änderung am Sourcecode umgesetzt werden muss, hat sich Java Instrumentation mit Javassist gut bewährt. Die Handhabung des einzufügenden Quellcodes sollte sorgfältig erfolgen – schließlich ist der Sourcecode nichts anderes als eine Zeichenkette. Das Einsatzspektrum von Javassist reicht von der Logging-Erweiterung über Profiling bis zum AOP. Es lohnt sich, sich mit dieser Technologie näher auseinanderzusetzen.

Links

https://docs.oracle.com/javase/7/docs/api/java/lang/instrument/package-summary.html

http://www.csg.is.titech.ac.jp/~chiba/javassist

http://en.wikipedia.org/wiki/Javassist

Andere Tools

http://jakarta.apache.org/bcel

http://asm.objectweb.org