18Dynamische Übersetzung und Skriptsprachen
»Wenn einer einen wirklich klaren Gedanken hat,
kann er ihn auch darstellen.«
– Michel de Montaigne (1533–1592)
Für Java-Entwickler besteht der Alltag im Allgemeinen darin, neuen Java-Quellcode zu schreiben und eigenen oder fremden Quellcode zu lesen, zu verstehen und zu ändern. Nun ist Java als Programmiersprache aber nicht immer die effizienteste Darstellungsform für einen Algorithmus oder ein Geschäftsproblem. Es gibt je nach Anwendungsfall kompaktere und bessere Ausdrucksformen. So kann eine grafische Notation für gewisse Problemstellungen sinnvoller sein als eine Gruppe von Java-Klassen, oder Tests können vielleicht besser in natürlicher Sprache formuliert werden. Installer oder Dialoge könnten alternativ in XML beschrieben werden. Besonders Skriptsprachen haben deutlich kompaktere Ausdrucksformen als Java. Ein Trend sind so genannte Domain Specific Languages (DSL). Das sind Spezial-Sprachen, die nur für ein ganz gewisses Problem definiert wurden und optimal ein Problem lösen sollen. So ist PDF eine optimale DSL für die Beschreibung von zweidimensionalen Grafiken, und Shell-Skripte sind perfekt zum Automatisieren von Vorgängen. Die Lösung hätte durchaus mit Java formuliert werden können, jedoch ist Java hierzu nicht optimal geeignet. Java ist als universell einsetzbare Programmiersprache eher das Gegenteil einer DSL. Und das Ziel sollte immer sein, das beste Werkzeug für eine Aufgabe zu wählen und viel zu automatisieren.
Da unabhängig von der Darstellungsform aber letztendlich eine Java-Laufzeitumgebung das Programm abarbeiten muss, stellt sich die Frage, wie die unterschiedlichen Darstellungsformen wie eine DSL ausgeführt werden, denn die JVM versteht nur Bytecode. Hier bieten sich zwei Wege an:
Ein Generator überführt die Darstellungsform in Java-Quellcode, der später wie die anderen Java-Klassen eines Projekts übersetzt wird. In einer erweiterten Form wird nicht nur Java-Quellcode erzeugt, sondern mit einem nachgeschalteten Compiler der Programmcode gleich übersetzt, sodass am Ende Klassendateien stehen oder sogar ein Java-Archiv. Die generierten Java-Klassen können vor dem Entwickler verborgen werden, indem sie zwar temporär generiert, aber nach dem Übersetzungsvorgang sofort wieder gelöscht werden. Der Nachteil des Generator-Ansatzes ist, dass im Build-Prozess ein extra Schritt nötig ist. Da der Entwickler aber immer sieht, was der Generator macht, ist dieser Prozess gut zu verstehen.
Die Beschreibung wird zur Laufzeit über einen Interpreter (oder Generator) verarbeitet. Es entstehen möglicherweise auch neue Klassendateien (vielleicht sogar mit dem Java-Standard-Compiler), aber das Generieren findet zur Laufzeit statt und nicht wie im ersten Fall vor dem Start des Programms. Es ist ein wenig mit der JVM vergleichbar, die Java-Programme in Maschinencode übersetzt. Traditionelle Compiler machen das vor dem Programmstart, und eine moderne JVM übersetzt dynamisch zur Laufzeitzeit.
In diesem Kapitel schauen wir uns genau diese Bereiche an. Zunächst wollen wir sehen, welche Möglichkeiten es gibt, Java-Programmcode zu generieren. Ob vor dem Programmstart oder dynamisch ist dann unerheblich. Der zweite Schritt soll das Problem lösen, wie (generierter) Java-Programmcode in Java-Bytecode übersetzt wird. Der letzte Teil des Kapitels stellt Skriptsprachen vor, von denen einige rein interpretiert werden und andere zur Laufzeit Bytecode erstellen, sodass sie von den Optimierungsmöglichkeiten der JVM profitieren.
18.1Codegenerierung
Der Ausgangspunkt der Codegenerierung ist ein Artefakt, das ein Übersetzer in Java-Quellcode oder Java-Bytecode überführt. Ob das Ergebnis Quellcode oder direkt Java-Bytecode ist, hat verschiedene Vor- und Nachteile:
Vorteil | Nachteil | |
---|---|---|
Generierung von Quellcode |
|
|
Direkte Bytecode-Generierung |
|
|
Tabelle 18.1Vor- und Nachteile vom Compiler und von direkter Bytecode-Erzeugung
Generatoren, die – aus welchem Zielformat auch immer – Java-Quellcode generieren, sind relativ einfach zu entwickeln. Der Aufwand besteht in der Entwicklung des Parsers, aber das Abbilden in einen Quellcode-String ist einfach. Ganz anders sieht es bei der Bytecode-Ausgabe aus. Zwar vereinfachen Bibliotheken das Generieren von Bytecode, doch bleibt eine deutlich höhere Komplexität bestehen. Der Vorteil ist jedoch, dass das Generieren und Verändern vom Bytecode recht performant ist und so problemlos zur Laufzeit vorgenommen werden kann. Sogar beim Laden der Klassen lassen sich Bytecode-Modifizierungen vornehmen, und in der Laufzeit macht sich das nicht auffällig negativ bemerkbar.
18.1.1Generierung von Java-Quellcode
Wir haben gesehen, dass ein Generator entweder Java-Quellcode oder Java-Bytecode generieren kann. Soll die Ausgabe ein Java-Programm im Quellcode sein, so lassen sich unterschiedliche Techniken zur Erzeugung nutzen:
Quellcode als Text | Quellcode über abstrakte Repräsentation | |
---|---|---|
Einfach | Template | |
Konkatenation von Strings oder Ausgaben über printXXX(…) | Ein Template enthält Platzhalter, die später gefüllt werden. | Es wird kein String erzeugt, sondern das Java-Programm wird über eine API aufgebaut. Nach dem Aufbau kann eine String-Repräsentation erzeugt werden. |
Tabelle 18.2Möglichkeiten zur Erzeugung von Quellcode
Generieren von Text
Im ersten Fall wird Java-Quellcode als String behandelt, der etwa über Konkatenation zusammengesetzt wird. Das kann so aussehen:
return "public class " +
classname +
"{ public static void main(String[] args){System.out.println(\"" +
output + "\");}}";
}
Der Nachteil ist offensichtlich und erinnert an Servlets, die HTML ausgeben: Das Programm selbst lässt sich kaum ändern, und der Programmautor verheddert sich schnell beim Escapen der Anführungsstriche. Besser ist es, das Java-Programm als Template zu nehmen und später Platzhalter mit den tatsächlichen Daten zu füllen. Die Java-Bibliothek bietet hier schon einen einfachen Mechanismus über String.format(…):
String template = "public class %1$s {" +
"public static void main(String[] args)" +
"{System.out.println(\"%2$s\");}}";
return String.format( template, classname, output );
}
Das Ganze können wir in eine Datei template.txt auslagern.
public static void main( String[] args ) {
System.out.println( "%2$s" );
}
}
Steht die Template-Datei im Klassenpfad, so greift das folgende generate(…) nun auf die Datei zurück und füllt die Platzhalter:
String template = new Scanner(getClass().getResourceAsStream( "template.txt" ))
.useDelimiter( "\\Z" ).next();
return String.format( template, classname, output );
}
Fortgeschrittener ist es, statt Positionen Namen einzusetzen. Dann ist auch String.format(…) nicht mehr nötig, sondern ein einfaches replace(…) ersetzt Platzhalter:
public static void main( String[] args ) {
System.out.println( "%OUTPUT%" );
}
}
Und die generate(…)-Methode wird zu:
String template = new Scanner(getClass().getResourceAsStream( "template.txt" ))
.useDelimiter( "\\Z" ).next();
return template.replace( "%CLASSNAME%", classname).replace( "%OUTPUT%", output );
}
Das Ganze lässt sich noch generalisieren, indem statt der zwei Parameter eine Map an die Methode übergeben wird. Anstatt dann zwei feste replace(…)-Aufrufe zu setzen, läuft eine Schleife über die Map und führt das Ersetzen durch, sodass der Schlüssel – Name des Platzhalters – durch den assoziierten Wert ersetzt wird.
Anstatt so etwas selbst zu programmieren, können wir auf vorgefertigte Lösungen zurückgreifen. Es bieten sich Template-Engines an, die neben einer einfachen Ersetzung noch viel mehr können, etwa Variablendeklarationen, Berechnungen, Schleifen, Makros. Bekannte Vertreter sind:
Apache Velocity (http://velocity.apache.org/)
FreeMarker (http://freemarker.org/)
StringTemplate (http://www.stringtemplate.org/)
Quellcodegenerierung über Modelle
Das Generieren von Text ist sehr einfach, doch prinzipiell mit Fehlern behaftet. Es kann sein, dass Bezeichner ungültig sind, Typen nicht passen oder Sonderzeichen eines Strings nicht richtig ausmaskiert sind. Das Herausschreiben von Text ist über die Vorlage praktisch, doch kann eine Bibliothek zum Generieren von Java-Quellcode helfen:
Eclipse Abstract Syntax Tree (AST) aus dem JDT: Eclipse bietet eine API, mit der sich Java-Programmcode aufbauen lässt.
CodeModel (http://codemodel.java.net/): Ein »Abfallprodukt« von JAXB, da JAXB mit xjc aus einem XML-Schema den Quellcode von JavaBeans generiert.
Nachdem der Syntax-Baum aufgebaut wurde, kann er in eine Textrepräsentation überführt werden. Die Eclipse-API ist angenehm, doch natürlich voll an den Eclipse-Compiler gebunden. Sprachänderungen der neuen Versionen kommen immer etwas verspätet. Die API von CodeModel ist nicht so komfortabel, und die Weiterentwicklung ist fragwürdig, da es lediglich als Unterprojekt von JAXB wirkt.
Fazit: Für ein einfaches Generieren von Quellcode sind die beiden APIs zu speziell. Templates sind sehr praktisch, wenn der Quellcode einem sehr festen Muster folgt, wie in unserem ersten Beispiel, in dem lediglich der Klassenname und ein String eingesetzt werden. Besser werden die API-Varianten, wenn der Quellcode weniger »frei« ist. Der größte Vorteil dieser Variante ist jedoch, dass sich der AST später einfacher modifizieren lässt. Auf der Basis von Quellcode ist das nicht möglich. Für Refactorings ist das optimal, aber um einfach Quellcode zu erzeugen, ist es etwas zu aufwändig.
18.1.2Codetransformationen
Bisher haben wir uns darauf konzentriert, Java-Quellcode über ein Java-Programm zu generieren. Es geht aber auch anders, etwa wenn der Ausgang ein XML-Dokument mit einer Beschreibung ist. Dann kann mittels XSLT eine Transformation von XML in ein Java-Programm stattfinden. Allerdings sind solche Transformationen nicht besonders angenehmen zu schreiben, doch möglich. XMLVM (http://www.xmlvm.org/) ist ein spannendes Projekt, das Java-Bytecode zunächst in XML abbildet. Eine anschließende XSLT-Transformation kann dann den Bytecode in ein anderes Zielformat überführen. Das Projekt ist aber nur ein Prototyp und seit Jahren inaktiv.
18.1.3Erstellen von Java-Bytecode
Ein Generator kann Java-Bytecode direkt erzeugen. Zwei bekannte APIs sind:
ASM (http://asm.ow2.org/)
Byte Code Engineering Library (Apache Commons BCEL; http://commons.apache.org/proper/commons-bcel/)
Beispiele finden sich auf den Webseiten. Insbesondere ASM ist beliebt, um Bytecode zur Laufzeit zu transformieren, um etwa bei der aspektorientierten Programmierung Aspekte, also Quellcode, einzuweben. Wir wollen das Erzeugen von Bytecode an dieser Stelle nicht weiter vertiefen und dem Compiler das Generieren von Bytecode überlassen.