© Excellent backgrounds/Shutterstock.com
Code statt dynamischer Proxies generieren

Es werde Code!


Warum nicht einen Proxy so generieren, wie man ihn benötigt? Aber wie fängt man an und was sollte beachtet werden? Es ist leichter, als man vermuten könnte.

Im JDK gibt es das Compiler-API. Dieses ermöglicht dem Entwickler, zur Laufzeit Java-Klassen selbst zu übersetzen. Eines der Haupteinsatzgebiete ist die Verwendung im Zusammenhang mit Codegeneratoren, die Java-Quelltexte erzeugen. Nach dem Übersetzungsvorgang kann man das Ergebnis über einen Class Loader direkt laden und verwenden. Prinzipiell gibt es verschiedene Vorgehensweisen. Wir werden uns hier den standardisierten Java-Compiler aus dem Package javax.tools ansehen. Dieser ist seit Java 6 im JDK enthalten und wurde erstmals im JSR-199 „Java Compiler API“ definiert. Um zur Laufzeit dynamisch Bytecode zu erzeugen, ist für uns die Möglichkeit von Interesse, aus einem String heraus Bytecode zu erzeugen und zu laden. Der String soll zur Laufzeit generiert und nicht aus einer Datei geladen werden.

Prinzipiell werden folgende Teile benötigt: Zum einen eine Klasse, die den Quellcode repräsentiert, in unserem Fall GeneratedJavaSourceFile. Zu beachten ist an dieser Stelle, dass die Repräsentation final in einer CharSequence vorliegen muss. Der Aufbau selbst ist recht simpel. Es wird ein URI angegeben, der hier lediglich aus einem Klassennamen mit der Sourcecode-Extension (Java) besteht, und der Typ Kind.SOURCE. Der Inhalt selbst wird in einem Attribut vom Typ CharSequence mit dem Namen javaSource gespeichert (Listing 1).

Listing 1

public class GeneratedJavaSourceFile extends SimpleJavaFileObject { private CharSequence javaSource; public GeneratedJavaSourceFile(String className, CharSequence javaSource) { super(URI.create(className + ".java"), Kind.SOURCE); this.javaSource = javaSource; } public CharSequence getCharContent( boolean ignoreEncodeErrors) throws IOException { return javaSource; } }

Das Gegenstück zum Quelltext ist der Bytecode. Dieser wird durch die Klasse GeneratedClassFile wiedergegeben. Der Aufbau ist ähnlich wie der von der Sourcecode-Repräsentation. Der URI enthält den Klassennamen mit der Extension class, und der übergebene Typ ist nun Kind.CLASS. Die generierte Klasse selbst wird im Attribut outputStream vom Typ ByteArrayOut­put­Stream gespeichert (Listing 2).

Listing 2

public class GeneratedClassFile extends SimpleJavaFileObject { private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); public GeneratedClassFile() { super(URI.create("generated.class"), JavaFileObject.Kind.CLASS); } public OutputStream openOutputStream() { return outputStream; } public byte[] getClassAsBytes() { return outputStream.toByteArray(); } }

In beiden Klassen wurde übrigens nicht die in der API-Doc angegebene URI-Schreibweise string:///Classname.java gewählt, da die hier verwendete ebenfalls bestens funktioniert.

Nun sind Quelle und Ziel definiert. Wie sieht die Verbindung dazwischen aus? Der Zugriff auf die Quelle und das Ziel erfolgt über den so genannten Java­File­Manager (Listing 3), den man sich wie den Dateimanager für den Compiler vorstellen kann. Normalerweise werden hier beim Standard­JavaFile­Manager die Zwischenschritte in temporären Dateien auf der Festplatte zwischengespeichert. Genau das wollen wir in unserem Fall nicht. Daher wird ein FileManager implementiert (Gen­er­atingJavaFileManager), der dieses ausschließlich im Speicher hält. Es wird lediglich das ByteArray weitergereicht.

Listing 3

public class GeneratingJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> { private final GeneratedClassFile gcf; public GeneratingJavaFileManager( StandardJavaFileManager sjfm, GeneratedClassFile gcf) { super(sjfm); this.gcf = gcf; } public JavaFileObject getJavaFileForOutput( Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { return gcf; } }

Zum Abschluss fehlt noch der Generator selbst. Wie wird also aus dem String der Bytecode? Hierzu sehen wir uns die Klasse Generator ein wenig genauer an.

Der Generator verwendet die private static native-Methode defineClass0 aus der Klasse Proxy, um die generierte Klasse direkt dem Class Loader hinzuzufügen. Sollte die Klasse schon vorhanden sein, wird eine Exeption geworfen. Die einzelnen Schritte sind schnell erklärt. Zuerst wird die Klasse kompiliert. Sollte das zu Fehlern führen, führt dieses in der Methode processResults dazu, dass die Fehler auf System.out geschrieben werden. Ist alles normal verlaufen, wird die Klasse erzeugt und mit dem Aufruf der Methode defineClass0 direkt dem Class Loader übergeben. Der Weg über die Methode defineClass0 wird genommen, damit nicht für jede neu generierte Klasse ein neuer Class Loader erzeugt werden muss.

Die Verwendung des Generators erfolgt, indem ein String, der den Java-Quelltext enthält, der Methode make(..) übergeben wird. Die zurückgelieferte Klasse kann dann per newInstance() dazu verwendet werden, neue Objekte zu erzeugen (Listing 4).

Listing 4

public class Generator { private static final Method defineClassMethod; private static final JavaCompiler jc; // snip init public static Class make(ClassLoader loader, String className, CharSequence javaSource) { GeneratedClassFile gcf = new GeneratedClassFile(); DiagnosticCollector<JavaFileObject> dc = new DiagnosticCollector<JavaFileObject>(); boolean result = compile(className, javaSource, gcf, dc); return processResults(loader, javaSource, gcf, dc, result); } private static boolean compile( String className, CharSequence javaSource, GeneratedClassFile gcf, Diagnost...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang