19.2 Programme mit der Compiler API übersetzen
Es gibt verschiedene Gründe, warum eigene Java-Programme selbst andere Programme übersetzen müssen. Ein zentraler Grund sind Codegeneratoren, die Java-Programme erzeugen; anschließend lassen sich die Exporte übersetzen und über einen eigenen Klassenlader direkt einbinden. Auch für eine IDE ist der Zugriff auf Compilermeldungen interessant. Sie kann Java-Programme übersetzen und mögliche Fehler live im Editor anzeigen.
Zum Übersetzen von Java-Programmen in Bytecode gibt es unterschiedliche Vorgehensweisen:
- Aufruf eines Compilers über einen externen Prozess. Das funktioniert mit jedem Compiler, ist aber langsam, da das Starten eines externen Prozesses immer problematisch ist. Zudem kann ein externer Compiler nicht den Quellcode aus dem Speicher lesen oder die Klassendateien im Speicher ablegen, sodass ein Klassenlader gleich Zugriff auf den Bytecode hat.
- Nutzen der standardisierten Java Compiler-API im Paket javax.tools seit Java 6.
- Rückgriff auf den Java-Compiler von Oracle oder Eclipse. Apache Commons Java Compiler Interface (JCI) unter http://tutego.de/go/jci ist eine Abstraktion, um mit einer API beide Compiler ansprechen zu können.
Wir gehen im Folgenden von einem JDK größer gleich Version 6 aus.
19.2.1 Java Compiler API
In Java 6 wurde eine Compiler API integriert, die erstmals im JSR-199, »Java Compiler API«, definiert wurde. Damit lässt sich der Java-Compiler über eine standardisierte API aufrufen und lassen sich Optionen wie Pfade oder Quellen setzen und Diagnosemeldungen einholen.
Wir wollen im folgenden Beispiel ein einfaches Programm entwickeln, das Java-Quellcode zur Laufzeit schreibt, diesen dann mit der Java Compiler API übersetzt und anschließend über einen eigenen Klassenlader lädt.
Das Generieren einer Datei mit dem Quellcode bereitet die wenigsten Schwierigkeiten:
Listing 19.1: com/tutego/insel/tools/CompileDemo.java, main() – Teil 1
File javaSrcFile = new File( "A.java" );
Writer p = new FileWriter( javaSrcFile );
p.write( "class A { static { System.out.println(\"Java Compiler API\"); } }" );
p.close();
Im nächsten Schritt ist die Compiler API gefragt:
Listing 19.2: com/tutego/insel/tools/CompileDemo.java, main() – Teil 2
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager( null, null, null );
Iterable<? extends JavaFileObject> units;
units = fileManager.getJavaFileObjectsFromFiles( Arrays.asList( javaSrcFile ) );
CompilationTask task = compiler.getTask( null, fileManager, null, null, null, units );
task.call();
fileManager.close();
Folgende Typen spielen mit:
- Der ToolProvider gibt mit der statischen Funktion getSystemJavaCompiler() ein Objekt vom Typ JavaCompiler zurück, das Zugang zum Compiler ermöglicht. (Sonst hat der ToolProvider nur noch eine zweite Methode, getSystemToolClassLoader() – es ist also noch Potenzial für eine API der anderen Tools).
- Vor der Übersetzung muss der Compiler initialisiert werden, etwa mit dem Ursprung der zu übersetzenden Quellen. Dazu wird dem Compiler ein JavaFileManager mitgegeben. Anstatt einen eigenen JavaFileManager zu entwickeln – das werden wir später machen, wenn die Klassen aus Bytefeldern stammen –, lässt sich vom JavaCompiler über getStandardFileManager() ein StandardJavaFileManager holen, der dateiorientiert arbeitet.
- Über den JavaFileManager weiß der Compiler, wo er die Quellen zu suchen hat und wohin er die Bytecodedateien schreiben soll, doch weiß er nicht, welche Klassen er genau übersetzen soll. Dazu ist jede zu übersetzende Compilationseinheit in ein JavaFileObject zu kapseln. Den Aufbau übernimmt in unserem Fall getJavaFileObjectsFromFiles(), das eine Sammlung von File-Objekten in JavaFileObject-Typen konvertiert. Die Alternative getJavaFileObjectsFromStrings() nimmt statt File-Objekten die Pfadnamen aus Strings.
- Mit dem JavaFileManager und der Liste der Compilationseinheiten in Form von JavaFileObject-Objekten liefert das JavaCompilerTool-Objekt anschließend mit getTask() ein JavaCompilerTool.CompilationTask-Objekt. Ein Aufruf von call() auf dem CompilationTask startet die Übersetzung.
Die Methode getTask() ist besonders lang und enthält viele null-Argumente. null ist immer erlaubt und setzt dann das Standardverhalten. An der Signatur der Methode lassen sich die Parameter besser ablesen:
CompilationTask getTask( Writer out,
JavaFileManager fileManager,
DiagnosticListener<? super JavaFileObject> diagnosticListener,
Iterable<String> options,
Iterable<String> classes,
Iterable<? extends JavaFileObject> compilationUnits );
Hinweis |
Als viertes Argument von getTask() lassen sich dem Compiler die üblichen Compiler-Optionen übergeben, also Pfade und sonstige Einstellungen: Iterable<String> options = Arrays.asList( "-verbose" ); |
Im letzten Schritt soll ein eigener Klassenlader die Klasse laden und anschließend den Quellcode löschen. Es folgt eine Ausgabe vom Klasseninitialisierer auf dem Bildschirm:
Listing 19.3: com/tutego/insel/tools/CompileDemo.java, main() – Teil 3
URLClassLoader classLoader = new URLClassLoader(
new URL[] { javaSrcFile.getAbsoluteFile().getParentFile().toURI().toURL() } );
Class.forName( "A", true, classLoader ); // Java Compiler API
javaSrcFile.delete();
Hinweis |
Der Typ CompilationTask ist methodenarm. Neben der call()-Methode gibt es gerade |
19.2.2 Fehler-Diagnose
Entwicklern ist leidvoll bekannt, dass die Übersetzung nicht immer gut geht. Der erste Hinweis ist die Rückgabe von call(); sie ist false, wenn die Übersetzung fehlschlägt. Des Weiteren bietet die Compiler API zwei Möglichkeiten, einen Statusbericht zu bekommen:
- Ein DiagnosticListener wird immer genau dann benachrichtigt, wenn der Compiler auf ein Problem stößt.
- Ein DiagnosticCollector ist ein vordefinierter DiagnosticListener, der alle Meldungen sammelt und später zugänglich macht.
Sehen wir uns an, was der DiagnosticCollector an Hinweisen liefert.
Listing 19.4: com/tutego/insel/tools/CompileWithDiagnosticsDemo.java, main()
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics =
new DiagnosticCollector<JavaFileObject>();
StandardJavaFileManager fileManager = compiler.getStandardFileManager( diagnostics, null, null );
Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjectsFromFiles(
Arrays.asList( new File("QQ.java"),
javaSrcFile ) );
CompilationTask task = compiler.getTask( null, fileManager, diagnostics, null, null, units );
boolean success = task.call();
fileManager.close();
System.out.println( success ); // false
for ( Diagnostic<?> diagnostic : diagnostics.getDiagnostics() )
{
System.out.printf( "Kind: %s%n", diagnostic.getKind() );
System.out.printf( "Quelle: %s%n", diagnostic.getSource() );
System.out.printf( "Code und Nachricht: %s: %s%n", diagnostic.getCode(),
diagnostic.getMessage( null ) );
System.out.printf( "Zeile: %s%n", diagnostic.getLineNumber() );
System.out.printf( "Position/Spalte: %s/%s%n", diagnostic.getPosition(),
diagnostic.getColumnNumber() );
System.out.printf( "Startpostion/Endposition: %s/%s%n", diagnostic.getStartPosition(),
diagnostic.getEndPosition() );
System.out.println();
}
Wir bitten den Compiler, eine nicht existierende Datei QQ.java zu übersetzen und folgenden Quellcode:
class A
{
staticccc
{
System.outprintln("Java Compiler API")
Die Reaktion vom Compiler sieht so aus:
false
Kind: ERROR
Quelle: QQ.java
Code und Nachricht: compiler.err.error.reading.file: error: error reading QQ.java; QQ.java (Das System kann die angegebene Datei nicht finden)
Zeile: –1
Position/Spalte: –1/-1
Startpostion/Endposition: –1/-1
Kind: ERROR
Quelle: A.java
Code und Nachricht: compiler.err.expected: A.java:3: <identifier> expected
Zeile: 3
Position/Spalte: 19/10
Startpostion/Endposition: 19/19
Kind: ERROR
Quelle: A.java
Code und Nachricht: compiler.err.expected: A.java:5: <identifier> expected
Zeile: 5
Position/Spalte: 39/18
Startpostion/Endposition: 39/39
Kind: ERROR
Quelle: A.java
Code und Nachricht: compiler.err.illegal.start.of.type: A.java:5: illegal start of type
Zeile: 5
Position/Spalte: 40/19
Startpostion/Endposition: 40/40
Kind: ERROR
Quelle: A.java
Code und Nachricht: compiler.err.premature.eof: A.java:5: reached end of file while
parsing
Zeile: 5
Position/Spalte: 60/39
Startpostion/Endposition: 60/60
Die Nutzung eines DiagnosticListener ist nicht viel anders. Anstatt den DiagnosticCollector als Parameter zu übergeben, wird eine DiagnosticListener-Implementierung übergeben. So kann sie aussehen:
Listing 19.5: com/tutego/insel/tools/ SimpleDiagnosticListener.java, SimpleDiagnosticListener
public class SimpleDiagnosticListener implements DiagnosticListener<JavaFileObject>
{
@Override
public void report( Diagnostic<? extends JavaFileObject> diagnostic )
{
// ...
}
}
19.2.3 Eine im String angegebene Compiliationseinheit übersetzen
Steht der Quellcode einer Compilationseinheit in einer Zeichenkette, so muss das Programm diese nicht erst in eine temporäre Datei schreiben. Zwar sieht die Compiler-API nicht direkt eine einfache Methode zum Übersetzen von Klassen aus Strings vor, doch viel an Infrastruktur ist schon vorhanden, sodass diese Funktionalität schnell realisiert ist.
Der erste Schritt ist die Entwicklung einer Unterklasse von JavaFileObject, die wir StringJavaFileObject nennen wollen. Das können wir dann wieder problemlos in eine Datenstruktur setzen und an getTask() übergeben. Die StringJavaFileObject-Klasse muss lediglich den Quellcode der Compilationseinheit über getCharContent() verfügbar machen.
Listing 19.6: com/tutego/insel/tools/StringJavaFileObject, StringJavaFileObject
public class StringJavaFileObject extends SimpleJavaFileObject
{
private final CharSequence code;
public StringJavaFileObject( String name, CharSequence code )
{
super( URI.create( "string:///" + name.replace( '.', '/' ) + Kind.SOURCE.extension ),
Kind.SOURCE );
this.code = code;
}
@Override
public CharSequence getCharContent( boolean ignoreEncodingErrors )
{
return code;
}
}
Der Konstruktor erwartet den Namen der zu übersetzenden Klasse und ein CharSequence (etwa String, StringBuilder) mit der Compilationseinheit. Der Klassenname wird an die Oberklasse weitergegeben. KIND ist eine in JavaFileObject deklarierte Aufzählung mit SOURCE (für Dateien mit der Endung .java), CLASS (für Klassendateien mit der Endung .class), HTML (für HTML-Dokumente) und OTHER.
Um den String mit dem Java-Quellcode in Bytecode zu übersetzen, wird die bekannte Zeile
Iterable<? extends JavaFileObject> units;
units = fileManager.getJavaFileObjectsFromFiles( Arrays.asList( javaSrcFile ) );
ersetzt durch:
Iterable<? extends JavaFileObject> units = Arrays.asList( javaFile );
Das gesamte Programm sieht so aus:
Listing 19.7: com/tutego/insel/tools/CompileFromStringDemo, main()
String src = "class A { static { System.out.println(\"Java Compiler API 2\"); } }";
StringJavaFileObject javaFile = new StringJavaFileObject( "A", src );
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager( null, null, null );
Iterable<? extends JavaFileObject> units = Arrays.asList( javaFile );
CompilationTask task = compiler.getTask( null, fileManager, null, null, null, units );
task.call();
fileManager.close();
URLClassLoader classLoader = new URLClassLoader( new URL[] { new File(".").getAbsoluteFile().toURI().toURL() } );
Class.forName( "A", true, classLoader ); // Java Compiler API 2
19.2.4 Wenn Quelle und Ziel der Speicher sind
Stammt nur der Quellcode aus dem Hauptspeicher, ist wenig Programmieraufwand nötig. Doch bisher legte der Compiler die Klassendateien immer auf dem Laufwerk in einen Ausgabeverzeichnis ab. Soll er nicht nur die Quellen aus dem Speicher beziehen, sondern ebenso die generierten Bytecode-Dateien im Speicher ablegen, ist mehr Aufwand nötig. Die Quellen (Units) gibt weiterhin eine Sammlung von JavaFileObject an, sodass hier nichts zu ändern ist. Jedoch ist ein neuer JavaFileManager nötig.
Das folgende Programm übersetzt eine Klassendeklaration aus einer Zeichenkette und schreibt in ein spezielles »Dateisystem«. Die gegenüber der letzten Version geänderten Stellen sind fett hervorgehoben:
Listing 19.8: com/tutego/insel/tools/CompileToMemoryDemo, main()
String src = "public class A { static { System.out.println(\"Java Compiler API 3\"); } }";
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
MemClassLoader classLoader = new MemClassLoader();
JavaFileManager fileManager = new MemJavaFileManager( compiler, classLoader );
JavaFileObject javaFile = new StringJavaFileObject( "A", src );
Collection<JavaFileObject> units = Collections.singleton( javaFile );
CompilationTask task = compiler.getTask( null, fileManager, null, null, null, units );
task.call();
fileManager.close();
Class.forName( "A", true, classLoader ).newInstance(); // Java Compiler API 3
Die nötigen Typen im Überblick
Zwei neue Klassen, MemJavaFileManager und MemClassLoader, sind im Beispiel sichtbar. Eine weitere interne Klasse MemJavaFileObject muss ebenfalls implementiert werden:
- MemJavaFileManager: Was im Text »Dateimanager« für den Compiler genannt wurde, ist natürlich ein spezieller JavaFileManager. Der Compiler ruft immer dann die Methode getJavaFileForOutput() auf dem speziellen JavaFileManager auf, wenn er eine Datei ablegen möchte. Es ist nun Aufgabe von MemJavaFileManager und der Methode MemJavaFileObject, dem Compiler ein JavaFileObject zu geben, das die Bytes der Klassendatei speichert.
- MemJavaFileObject ist genau so ein JavaFileObject, das der MemJavaFileManager bei getJavaFileForOutput() liefert. Der Compiler ruft die von MemJavaFileObject überschriebene Methode openOutputStream() auf und bekommt so die Möglichkeit, die Klassendatei herauszuschreiben. An der Stelle würden wir auch ansetzen, wenn der Compiler einen anderen Speicherort wählen sollte, etwa eine Datenbank, oder den Bytecode gleich verschlüsselt ablegen soll.
- MemClassLoader ist ein Klassenlader und das letzte Teilchen. Der erzeugte Bytecode pro Klasse oder Schnittstelle steht ja in einem MemJavaFileObject-Objekt. Der eigene Klassenlader macht diese erzeugen Bytefelder für das Class.forName() zugänglich.
Zu den drei Klassen:
Listing 19.9: com/tutego/insel/tools/MemJavaFileManager.java
package com.tutego.insel.tools;
import javax.tools.*;
import javax.tools.JavaFileObject.Kind;
public class MemJavaFileManager extends
ForwardingJavaFileManager<StandardJavaFileManager>
{
private final MemClassLoader classLoader;
public MemJavaFileManager( JavaCompiler compiler, MemClassLoader classLoader )
{
super( compiler.getStandardFileManager( null, null, null ) );
this.classLoader = classLoader;
}
@Override
public JavaFileObject getJavaFileForOutput( Location location,
String className,
Kind kind,
FileObject sibling )
{
MemJavaFileObject fileObject = new MemJavaFileObject( className );
classLoader.addClassFile( fileObject );
return fileObject;
}
}
Listing 19.10: com/tutego/insel/tools/MemJavaFileObject.java
package com.tutego.insel.tools;
import java.io.*;
import java.net.*;
import javax.tools.*;
class MemJavaFileObject extends SimpleJavaFileObject
{
private final ByteArrayOutputStream baos = new ByteArrayOutputStream( 8192 );
private final String className;
MemJavaFileObject( String className )
{
super( URI.create( "string:///" + className.replace( '.', '/' ) + Kind.CLASS.extension ),
Kind.CLASS );
this.className = className;
}
String getClassName()
{
return className;
}
byte[] getClassBytes()
{
return baos.toByteArray();
}
@Override public OutputStream openOutputStream()
{
return baos;
}
}
Listing 19.11: com/tutego/insel/tools/MemClassLoader.java
package com.tutego.insel.tools;
import java.util.*;
public class MemClassLoader extends ClassLoader
{
private final Map<String, MemJavaFileObject> classFiles =
new HashMap<String, MemJavaFileObject>();
public MemClassLoader()
{
super( ClassLoader.getSystemClassLoader() );
}
public void addClassFile( MemJavaFileObject memJavaFileObject )
{
classFiles.put( memJavaFileObject.getClassName(), memJavaFileObject );
}
@Override
protected Class<?> findClass( String name ) throws ClassNotFoundException
{
MemJavaFileObject fileObject = classFiles.get( name );
if ( fileObject != null )
{
byte[] bytes = fileObject.getClassBytes();
return defineClass( name, bytes, 0, bytes.length );
}
return super.findClass( name );
}
}
Ihr Kommentar
Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.