Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Vorwort
1 Neues in Java 7
2 Threads und nebenläufige Programmierung
3 Datenstrukturen und Algorithmen
4 Raum und Zeit
5 Dateien, Verzeichnisse und Dateizugriffe
6 Datenströme
7 Die eXtensible Markup Language (XML)
8 Dateiformate
9 Grafische Oberflächen mit Swing
10 Grafikprogrammierung
11 Netzwerkprogrammierung
12 Verteilte Programmierung mit RMI
13 RESTful und SOAP Web-Services
14 JavaServer Pages und Servlets
15 Applets
16 Datenbankmanagement mit JDBC
17 Technologien für die Infrastruktur
18 Reflection und Annotationen
19 Dynamische Übersetzung und Skriptsprachen
20 Logging und Monitoring
21 Java Native Interface (JNI)
22 Sicherheitskonzepte
23 Dienstprogramme für die Java-Umgebung
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
Java 7 - Mehr als eine Insel von Christian Ullenboom
Das Handbuch zu den Java SE-Bibliotheken
Buch: Java 7 - Mehr als eine Insel

Java 7 - Mehr als eine Insel
Rheinwerk Computing
1433 S., 2012, geb.
49,90 Euro, ISBN 978-3-8362-1507-7
Pfeil 19 Dynamische Übersetzung und Skriptsprachen
Pfeil 19.1 Codegenerierung
Pfeil 19.1.1 Generierung von Java-Quellcode
Pfeil 19.1.2 Codetransformationen
Pfeil 19.1.3 Erstellen von Java-Bytecode
Pfeil 19.2 Programme mit der Compiler API übersetzen
Pfeil 19.2.1 Java Compiler API
Pfeil 19.2.2 Fehler-Diagnose
Pfeil 19.2.3 Eine im String angegebene Compiliationseinheit übersetzen
Pfeil 19.2.4 Wenn Quelle und Ziel der Speicher sind
Pfeil 19.3 Ausführen von Skripten
Pfeil 19.3.1 JavaScript-Programme ausführen
Pfeil 19.3.2 Groovy
Pfeil 19.4 Zum Weiterlesen

Rheinwerk Computing - Zum Seitenanfang

19.2 Programme mit der Compiler API übersetzenZur nächsten Überschrift

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.


Rheinwerk Computing - Zum Seitenanfang

19.2.1 Java Compiler APIZur nächsten ÜberschriftZur vorigen Überschrift

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" );
CompilationTask task = compiler.getTask( null, fileManager, null, options, null, units );

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
einmal setLocale() zum Setzen der Sprache für Fehler- und Diagnosemeldungen sowie setProcessors(). Die letzte Methode ermöglicht die Zuweisung von Annotations-Prozessoren. Mit ihnen haben wir uns schon in —Kapitel 18, »Reflection und Annotationen«, beschäftigt. Die JSR-269, »Pluggable Annotation Processing API«, wurde wie die JSR-199, »Java Compiler API«, in Java 6 eingeführt.


Rheinwerk Computing - Zum Seitenanfang

19.2.2 Fehler-DiagnoseZur nächsten ÜberschriftZur vorigen Überschrift

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 )
{
// ...
}
}

Rheinwerk Computing - Zum Seitenanfang

19.2.3 Eine im String angegebene Compiliationseinheit übersetzenZur nächsten ÜberschriftZur vorigen Überschrift

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

Rheinwerk Computing - Zum Seitenanfang

19.2.4 Wenn Quelle und Ziel der Speicher sindZur nächsten ÜberschriftZur vorigen Überschrift

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.

>> Zum Feedback-Formular
<< zurück
  Zum Katalog
Neuauflage: Java SE 8 Standard-Bibliothek
Neuauflage: Java SE 8 Standard-Bibliothek
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Professionell entwickeln mit Java EE 7






 Professionell
 entwickeln mit
 Java EE 7


Zum Katalog: Java ist auch eine Insel






 Java ist auch
 eine Insel


Zum Katalog: Einstieg in Eclipse






 Einstieg in Eclipse


Zum Katalog: Einstieg in Java






 Einstieg in Java


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2012
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de