JAVA-Klassen ohne LANG zu überlegen INSTRUMENTieren
Dieser Artikel wurde von Tobias Nebel 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 Programmier-Paradigmen wie die Aspekt-orientierte Programmierung (AOP) realisiert werden. Der Vorgang des Veränderns von Bytecode wird in der Softwaretechnologie auch als Bytecode-Engineering oder Instrumentierung bezeichnet. Eine sinnvolle Erweiterung in Java 5 stellt das Package java.lang.instrument dar, welches eine einfache Integration von Bytecode-Engineering-Mechanismen ermöglicht.
Der Vorteil der Bytecode-Manipulation besteht darin, dass kein erneutes Compilieren des Quellcodes notwendig ist. Ebenso vorteilhaft ist, dass der Quellcode eines Programmes nicht zwangsläufig zur Manipulation vorhanden sein muss. Je nach Lizenz eines Softwareproduktes ist ein Zugang zum Quellcode zuweilen gar nicht möglich. Neben der Veränderung von Java Bytecode ist es genauso auch möglich, mittels Bytecode-Engineering Klassen komplett neu zu generieren. In diesem Fall würde man die Notwendigkeit eines Compilers komplett umgehen.
Durch Analysieren und Modifizieren des Bytecodes können spezielle Punkte im Programm mit zusätzlicher Funktionalität versehen oder verändert werden. Ein einfaches Beispiel dafür ist das Einfügen von Zeitstempel-Ausgaben an bestimmten Programmpunkten zur Zeitmessung.
Zur Einbindung von Bytecode-Engineering in ein System stellen sich zwei gegensätzliche Ansätze heraus:
Bei der statischen Instrumentierung wird eine class-Datei lediglich einmal instrumentiert und anschließend gespeichert. Diese Variante ist jedoch nicht immer möglich bzw. auch nicht in jedem Falle ausreichend, da sich gegebenenfalls auch die Art der Instrumentierung ändern könnte. Weiterhin ist es sinnvoller, statische Veränderungen am Quellcode direkt vorzunehmen.
Die dynamische Instrumentierung ist das Bearbeiten vorhandenen Bytecodes jeweils zur Ladezeit einer Klasse. Da Bytecode-Engineering im Gegensatz zur Verwendung eines Compilers erhebliche Vorteile bei der Performanz bietet, ist eine sinnvolle Verwendung des dynamischen Instrumentierens zur Laufzeit eines Programmes durchaus möglich.
Die Einbindung eines Instrumentierungs-Mechanismus, welcher beispielsweise mittels eines der im Artikel über Bytecode-Engineering vorgestellten Frameworks realisiert werden kann, ist am einfachsten mit dem seit Java 5 vorhandenen Package java.lang.instrumentmöglich.
Der erste notwendige Schritt ist die Implementierung einer so genannten premain()-Methode. Wie der Name vermuten lässt, wird diese noch vor der eigentlichen main()-Methode aufgerufen. Die Klasse, die diese Methode enthält, wird auch als Agentenklasse oder als Agent bezeichnet. Die premain()-Methode hat zwei Argumente: einen String mit den Parametern für die Agentenklasse und eine Instanz der Klasse Instrumentation. Dieser Instanz können nun Objekte vom Typ ClassFileTransformer mittels addTransformer() übergeben werden:
public static void premain(String agentArgs, Instrumentation inst)
{
inst.addTransformer( new MyClassFileTransformer() );
}
Eine Klasse, welche das Interface ClassFileTransformer implementiert, muss die abstrakte Methode transform() implementieren. Dieser Methode wird unter anderem das ursprüngliche Byte-Array der zu ladenden Klasse übergeben. Nun kann dieses Byte-Array nach eigenem Bedarf verändert werden. Hier sei wieder auf die bereits vorgestellten Frameworks zur Bytecode-Manipulation verwiesen. Der Rückgabewert der transform()-Methode ist letztendlich wieder ein Byte-Array, welches den instrumentierten Bytecode enthält:
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer )
throws IllegalClassFormatException
{
byte[] result = modify(classfileBuffer);
return result;
}
Um den Instrumentierungsmechanismus mittels des Agenten und der darin implementierten premain()-Methode überhaupt nutzen zu können, bedarf es weiterer Schritte. Die erste Notwendigkeit besteht darin, die AgentClassund die instrumentierenden Klassen in eine jar-Datei zu packen. Diesem Archiv ist in seiner Manifest-Datei der folgende Parameter hinzuzufügen:
Premain-Class: <agentpackage.AgentClass>
Dieser Parameter zeigt auf die Agenten-Klasse, welche die premain()-Methode implementiert.
Für die Einbindung der Instrumentierung mittels des erstellten Archivs ist dieses Archiv der JVM als Agenten-Argument mitzuteilen. Dies geschieht über das JVM-Argument
-javaagent:<agentarchive>.jar
Damit werden alle Klassen vor ihrer Initialisierung dem ClassFileTransformer übergeben, der dann die Instrumentierung durchführen kann.
Der Instrumentierungsmechanismus des Packages java.lang.instrument setzt am ClassLoader der Applikation, also direkt nach dem System-ClassLoader an. Daraus resultiert, dass viele Klassen beim Laden in der transform()-Methode der Instrumentierung unterworfen werden. Dazu zählen teilweise auch Klassen aus den Packages java.utilund java.lang. Es ist somit sinnvoll, an geeigneter Stelle, wie etwa in einem Property-File, festzulegen, welche Klassen instrumentiert werden dürfen und welche nicht.
Um gleich loslegen zu können, ist hier eine Beispiel-Klasse zu finden, in der das Grundgerüst für eine Instrumentierung bereitgestellt ist. Die modify(byte[])-Methode ist nun der Ansatzpunkt für eigene Implementierungen.
























Christoph Schmidt sagt
am 25. November 2008 @ 18:18
Interessante Sache, wobei, wenn ich mir das Rudiment so anschaue, ja doch die Frage nach der notwendigen Mächtigkeit der modify()-Methode bleibt.
Um darin sinnvoll instrumentieren zu können, muss man sich entweder auf ‘markante’ ByteCodes verlassen (soweit ich mich erinnere gibt es ein paar reservierte, aber nicht genutzte ByteCodes in der Spec – wäre mal interessant zu wissen, was eine Standard-JRE mit einem solchen ByteCode macht – sofern die JRE da nicht streikt, könnte man für die Instrumentierung eventuell dort ansetzen.) oder den übergebenen Code selbst vollständig analysieren – beliebig mächtige Instrumentierung benötigt am Ende also vielleicht doch die selbe mächtige (aufwendige) Analyse des Codes wie beim Kompilieren. Ich kann mir dementsprechend gut vorstellen, dass beliebig mächtige Instrumentierungen nicht _viel_ schneller sind als die Arbeit mit dem Compiler. Allerdings bleibt natürlich der Dynamik-Aspekt als großer Vorteil..
Viele Grüße
Tobias Nebel sagt
am 25. November 2008 @ 22:32
@Christoph
Also prinzipiell wollte ich hier nur einen Einstiegspunkt in Bytecode-Engineering aufzeigen. Detailiertere Betrachtungen sind insofern schwierig, da diese in die verschiedensten Richtungen ausschweifen könnten:
eine Instrumentierung könnte ja eine Vielzahl erdenklicher Veränderungen am vorhandenen Bytecode vornehmen…von Analysen, wie sie Profiler durchführen, über ergänzende Funktionalitäten, wie in der Aspekt-orientierten Programmierung, bis hin zur kompletten Neugenerierung von Klassen oder -bestandteilen ist da mehr drin, als man sich auf die Schnelle einfallen lassen kann. Hier sei ‘mal dezent auf meinen nächsten Blog-Artikel verwiesen
Also was den Performanz-Vorteil gegenüber Compilern angeht geb’ ich dir soweit Recht, dass eine umfangreiche Analyse echt Zeit frisst! Wenn allerings überschaubare Sachen insturmentiert werden, dann kommst du sicher ohne Compiler besser. Abseits von der Performanz hast du durch die Bytecode-Analyse natürlich noch die Möglichkeit, Vorgänge auf der Bytecode-Ebene in die Java Sprachebene zu heben (du kannst so beispielsweise das Laden von Werten auf den Stack nachvollziehen).
Was unimplementierte Bytecode-Befehle angeht kann ich dir nicht wirklich viel sagen, da diese selbst für mich Neuland sind. Laut Spec sind diese Befehle einfach nur “reserviert” und “derzeitig unimplementiert”. Meine Vermutung wäre deshalb, dass (zumindest was die “Volks-VM” angeht) hier einfach garnichts passiert.
Viele Grüße!
…und ich hoffe dass wir jetzt ‘ne heiße Diskussion losgerissen haben
Metadaten, oder: Warum man Volt und milliVolt nicht einfach addieren kann. | universal.adapter sagt
am 9. Dezember 2008 @ 9:49
[...] Mechanismus’ (siehe java.lang.instrument) eingewoben. Eine Einführung in diese Thematik ist hier zu finden. Um nun die Konsistenz zwischen Daten und Metadaten zu wahren muss einfach jeder [...]
Boris Petrovic sagt
am 27. April 2009 @ 14:15
Beitrag zum Thema finde ich sehr interesant. Ich habe auch manche Überlegungen mit java.lang.instrument in diese Richtung gemacht. Mir blieb immer dieser lätzten Schritt modify painlich. Bytecodes auswendig zu lernen finde ich nicht besonders toll. BCEL und änlichen Frameworks scheinen nicht kompatibel mit java.lang.instrument zu sein (oder täusche ich mich).
Wäre sehr insteresant ein konkretes Beispiel mit modify zu sehen.
Gruss
Boris