23.3Programmieren mit der Tools-API
Tools wie der Java-Compiler javac, aber auch jarsigner, javadoc, xjc, javap und andere Kommandozeilenwerkzeuge, sind rein in Java geschrieben. Einige Tools bieten Entwicklern eine API, sodass sie erweitert und von Programmen angesprochen werden können. Das ist interessant für Tool-Anbieter, die zum Beispiel den Compiler erweitern oder die Ausgabe vom javadoc-Tool anpassen möchten.
23.3.1Java-Tools in Java implementiert
Alle in Java geschriebenen Programme befinden sich in einem Extra-Java-Archiv mit dem Namens tools.jar, das sich im lib-Verzeichnis des JDK befindet. Das JDK liefert allerdings nur die Klassendateien mit aus, die Quellen mit Javadoc müssen sich Interessierte selbst aus dem Mercurial Repository[ 154 ](Für Java 8 liefert http://hg.openjdk.java.net/jdk8/jdk8/langtools/ den Zugriff auf die Quellen. Sie lassen sich auch in einem Rutsch in unterschiedlichen Archivformaten, auch ZIP, beziehen.) holen, denn sie sind nicht im üblichen src.zip vom JDK mit dabei.
Die normalen ausführbaren Programme wie javadoc oder javah haben (unter Windows) alle die gleiche Größe um 15 KiB, da sie nichts anderes machen, als die Java-Laufzeitumgebung mit tools.jar im Klassenpfad aufzurufen und dann an die entsprechende main(String[])-Methode des Programms weiterzuleiten. Statt javadoc aufzurufen, kommt com.sun.tools.javadoc.Main.main(args) zum gleichen Ziel.
23.3.2Tools aus eigenen Java-Programmen ansprechen
Um ein Werkzeug direkt aus einem Java-Programm anzusprechen, gibt es zwei Möglichkeiten:
Die öffentliche Klasse javax.tools.ToolProvider bietet eine API, um Zugang zum Compiler zu bekommen und um das javadoc-Tool aufzurufen, mit dem die Java-Dokumentation erstellt oder die Javadoc-Kommentare verarbeitet werden können. Das Problem: Nur zwei Tools sind zugänglich, keine weiteren Tools wie javap, xjc, …
Aufrufe der Tool-Einstiegsklassen, etwa com.sun.tools.javadoc.Main.main(…). Da alle Tools über eine solche Klasse verfügen, ist ein direkter Aufruf aus einem Java-Programm immer möglich, aber nicht unproblematisch, da Entwickler sich hier von den internen Paketstrukturen abhängig machen.
23.3.3API-Dokumentation der Tools
Die öffentliche Klasse javax.tools.ToolProvider ist Teil der Java SE und folglich dokumentiert, so wie auch der Zugang zum Compiler und zum javadoc-Werkzeug. Ein Beispiel für die Übersetzung von Java-Quellen zeigte Kapitel 18, »Dynamische Übersetzung und Skriptsprachen«.
Die anderen JDK-Tools, die Entwickler aus Java heraus ansprechen können, haben ebenfalls API-Dokumentationen, die aber nicht zur Java SE-Standarddokumentation gehören, sondern in der Dokumentations-ZIP unter docs\jdk\api liegen. Dort lassen sich alle Details ablesen, wie etwa der Java-Compiler angesprochen wird oder wie Programme, die aus der Java-API-Dokumentation zum Beispiel verlinkte HTML-Dokumente aufbauen, so genannte Doclets, geschrieben werden. Oracle definiert nicht für jedes Tool eine öffentliche API. So ist es zum Beispiel nicht vorgesehen, in den Erzeugungsprozess von javah einzugreifen, und Oracle dokumentiert diese Eigenschaft in den Quellen auch wie folgt:
»This is NOT part of any supported API. If you write code that depends on this, you do so at your own risk. This code and its internal interfaces are subject to change or deletion without notice.«
23.3.4Eigene Doclets
Das javadoc-Tool hat die Aufgabe, den Java-Quellcode auszulesen und die Javadoc-Kommentare zu extrahieren. Was dann mit den Daten passiert, ist die Aufgabe eines Doclets. Das Standard-Doclet von Oracle erzeugt die bekannte Struktur auf verlinkten HTML-Dateien, aber es lassen sich auch eigene Doclets schreiben, um etwa in Javadoc-Kommentaren enthaltene Links auf Erreichbarkeit zu prüfen oder UML-Diagramme aus den Typbeziehungen zu erzeugen.
Die Doclet-API ist nicht Teil der Java-Standardbibliothek und auch nicht im Klassenpfad eingebunden. Um eigene Doclets zu schreiben, muss zunächst tools.jar in den Klassenpfad aufgenommen werden. Teil des Java-Archivs ist das Paket com.sun.javadoc, was Typen und Methoden deklariert, mit denen sich besuchte Pakete, Klassen, Methoden usw. und deren Javadoc-Texte erfragen lassen.
Im folgenden Beispiel wollen wir ein kleines Doclet schreiben, das Klassen, Methoden und Konstruktoren ausgibt, die das Tag @since 1.8 tragen (bzw. @since 8, was aber eigentlich falsch ist). So lässt sich leicht ermitteln, was in der Version Java 8 alles hinzugekommen ist. Doclets werden normalerweise von der Kommandozeile aufgerufen und dem javadoc-Tool übergeben. Unser Programm vereinfacht das, indem es direkt das Tool über Java mit dem passenden Parameter aufruft. tools.jar muss dafür im Klassenpfad sein und die Dokumentation ausgepackt am angegeben Ort.
Listing 23.2com/tutego/tools/javadoc/SinceJava8FinderDoclet.java
import java.util.*;
import java.util.function.Predicate;
import com.sun.javadoc.*;
import com.sun.tools.javadoc.Main;
public class SinceJava8FinderDoclet {
public static boolean start( RootDoc root ) {
for ( ClassDoc clazz : root.classes() )
processClass( clazz );
return true;
}
private static void processClass( ClassDoc clazz ) {
Predicate<Tag> isJava18 = tag -> tag.text().matches( "(1\\.)?8" );
if ( Arrays.stream( clazz.tags( "since" ) ).anyMatch( isJava18 ) )
System.out.printf( "Neuer Typ %s%n", clazz );
for ( MethodDoc method : clazz.methods() )
if ( Arrays.stream( method.tags( "since" ) ).anyMatch( isJava18 ) )
System.out.printf( "Neue Methode %s%n", method );
for ( ConstructorDoc constructor : clazz.constructors() )
if ( Arrays.stream( constructor.tags( "since" ) ).anyMatch( isJava18 ) )
System.out.printf( "Neuer Konstruktor %s%n", constructor );
for ( FieldDoc field : clazz.fields() )
if ( Arrays.stream( field.tags( "since" ) ).anyMatch( isJava18 ) )
System.out.printf( "Neues Attribut %s%n", field );
}
public static void main( String[] args ) {
String[] params = {
"-quiet", "-XDignore.symbol.file",
"-doclet", SinceJava8FinderDoclet.class.getName(),
"-sourcepath", "C:/Program Files/Java/jdk1.8.0/src/",
// "java.lang", // Nur java.lang
"-subpackages", "java:javax" // Alles rekursiv unter java.* und javax.*
};
Main.execute( params );
}
}
Unsere main(String[])-Methode ruft das JDK-Doclet-Programm über Main.execute(String[]) auf und übergibt die eigene Doclet-Klasse per Parameter – die Argumente von execute(String[]) erinnern an die Kommandozeilenparameter. Das Doclet-Hauptprogramm wiederum ruft unsere start(RootDoc root)-Methode auf – das Gleiche würde auch passieren, wenn das Doclet von außen über javadoc aufgerufen würde. Unser start(RootDoc) läuft über alle ermittelten Typen und übergibt zum Abarbeiten der Innereien die Verantwortung an processClass(ClassDoc). Die Metadaten kommen dabei über diverse XXXDoc-Typen. Ein Precidate zieht den Tag-Test heraus, ein weiterer Einsatz der neuen Java 8-Streams würde das Programm nicht übersichtlicher machen.
Das Programm nutzt ein paar Tricks, um die Ausgabe auf das Wesentliche zu konzentrieren. Der Schalter -quit schaltet den üblichen Ladestatus ab, der zu Ausgaben wie
Loading source files for package java.applet...
Loading source files for package java.awt...
führt.
Der Schalter -XDignore.symbol.file wiederum unterdrückt Meldungen wie diese hier:
be removed in a future release
import sun.misc.Unsafe;
^
Die Meldungen landen auf dem System.err-Kanal, sodass sie auch mit System.setErr(…) in einen ausgabeverwerfenden Strom geschickt werden können, um sie zu unterdrücken.
23.3.5Auf den Compiler-AST einer Klasse zugreifen
Der Zugriff auf den Java-Compiler ist über die Java-Compiler-API standardisiert, jedoch sind alle Interna, wie die tatsächliche Repräsentation des Programmcodes verborgen. Die Compiler-API abstrahiert alles über Schnittstellen, und so kommen Entwickler nur mit JavaCompiler, StandardJavaFileManager und CompilationTask in Kontakt – alles Schnittstellen aus dem Paket javax.tools. Um etwas tiefer einzusteigen, lässt sich zu einem Trick greifen: Klassen implementieren Schnittstellen, und wenn ein Programm den Schnittstellentyp auf den konkreten Klassentyp anpasst, dann stehen in der Regel mehr Methoden zur Verfügung. So lässt sich der CompilationTask auf eine com.sun.tools.javac.api.JavacTaskImpl casten, und dann steht eine parse()-Methode für Verfügung. Die parse()-Methode liefert als Rückgabe eine Aufzählung von CompilationUnitTree. Damit stehen wir direkt in der internen Repräsentation, die der Compiler von einem Programmcode aufgebaut hat. Er nennt sich Abstract Syntax Tree (AST). Um diesen Baum nun abzulaufen, lässt sich das Besuchermuster einsetzen. CompilationUnitTree bietet eine accept(…)-Methode, der übergeben wir einen TreeScanner mit Callback-Methoden. Die accept(…)-Methode ruft dann beim Ablaufen jedes Knotens unseren Besucher auf:
Listing 23.3com/tutego/tools/javac/PrintAllMethodNames.java
import java.io.*;
import java.net.*;
import javax.tools.*;
import javax.tools.JavaCompiler.CompilationTask;
import com.sun.source.tree.*;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.api.JavacTaskImpl;
public class PrintAllMethodNames {
final static TreeScanner<?, ?> methodPrintingTreeVisitor = new TreeScanner<Void, Void>() {
@Override public Void visitCompilationUnit( CompilationUnitTree unit, Void arg ) {
System.out.println( "Paket: " + unit.getPackageName() );
return super.visitCompilationUnit( unit, arg );
};
@Override public Void visitClass( ClassTree classTree, Void arg ) {
System.out.println( "Klasse: " + classTree.getSimpleName() );
return super.visitClass( classTree, arg );
}
@Override public Void visitMethod( MethodTree methodTree, Void arg ) {
System.out.println( "Methode: " + methodTree.getName() );
return super.visitMethod( methodTree, arg );
}
};
public static void main( String[] args ) throws IOException, URISyntaxException {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = ¿
compiler.getStandardFileManager( null, null, null );
URI filename = ¿
PrintAllMethodNames.class.getResource( "PrintAllMethodNames.java" ).toURI();
Iterable<? extends JavaFileObject> fileObjects = fileManager.getJavaFileObjects( new File( filename ) );
CompilationTask task = compiler.getTask( null, null, null, null, null, fileObjects );
JavacTaskImpl javacTask = (JavacTaskImpl) task;
for ( CompilationUnitTree tree : javacTask.parse() )
tree.accept( methodPrintingTreeVisitor, null );
}
}
Ein TreeScanner hat viele Methoden, wir interessieren uns nur für den Start einer Kompilationseinheit für den Paketnamen, für alle Klassen und Methoden. Wir könnten uns aber auch über alle Annotationen oder do-while-Schleifen informieren lassen. Die Ausgabe ist:
Klasse: PrintAllMethodNames
Klasse:
Methode: visitCompilationUnit
Methode: visitClass
Methode: visitMethod
Methode: main
Die zweite Angabe für den Klassennamen ist leer, da die anonyme Klasse eben keinen Namen hat.