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 8 und Java 7
2 Fortgeschrittene String-Verarbeitung
3 Threads und nebenläufige Programmierung
4 Datenstrukturen und Algorithmen
5 Raum und Zeit
6 Dateien, Verzeichnisse und Dateizugriffe
7 Datenströme
8 Die eXtensible Markup Language (XML)
9 Dateiformate
10 Grafische Oberflächen mit Swing
11 Grafikprogrammierung
12 JavaFX
13 Netzwerkprogrammierung
14 Verteilte Programmierung mit RMI
15 RESTful und SOAP-Web-Services
16 Technologien für die Infrastruktur
17 Typen, Reflection und Annotationen
18 Dynamische Übersetzung und Skriptsprachen
19 Logging und Monitoring
20 Sicherheitskonzepte
21 Datenbankmanagement mit JDBC
22 Java Native Interface (JNI)
23 Dienstprogramme für die Java-Umgebung
Stichwortverzeichnis

Jetzt Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Java SE 8 Standard-Bibliothek von Christian Ullenboom
Das Handbuch für Java-Entwickler
Buch: Java SE 8 Standard-Bibliothek

Java SE 8 Standard-Bibliothek
Pfeil 18 Dynamische Übersetzung und Skriptsprachen
Pfeil 18.1 Codegenerierung
Pfeil 18.1.1 Generierung von Java-Quellcode
Pfeil 18.1.2 Codetransformationen
Pfeil 18.1.3 Erstellen von Java-Bytecode
Pfeil 18.2 Programme mit der Java Compiler API übersetzen
Pfeil 18.2.1 Java Compiler API
Pfeil 18.2.2 Fehlerdiagnose
Pfeil 18.2.3 Eine im String angegebene Kompilationseinheit übersetzen
Pfeil 18.2.4 Wenn Quelle und Ziel der Speicher sind
Pfeil 18.3 Ausführen von Skripten
Pfeil 18.3.1 Java-Programme mit JavaScript schreiben
Pfeil 18.3.2 Kommandozeilenprogramme jrunscript und jjs
Pfeil 18.3.3 javax.script-API
Pfeil 18.3.4 JavaScript-Programme ausführen
Pfeil 18.3.6 Alternative Sprachen für die JVM
Pfeil 18.3.7 Von den Schwierigkeiten, dynamische Programmiersprachen auf die JVM zu bringen *
Pfeil 18.4 Zum Weiterlesen
 
Zum Seitenanfang

18.2Programme mit der Java Compiler API übersetzen Zur vorigen ÜberschriftZur 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:

  • Nutzen der standardisierten Java Compiler API im Paket javax.tools

  • 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.

  • 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.

Wir greifen im Folgenden auf die standardisierte Java Compiler API zurück.

 
Zum Seitenanfang

18.2.1Java Compiler API Zur vorigen ÜberschriftZur nächsten Ü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 18.1com/tutego/insel/tools/CompileDemo.java, main() Teil 1

Path javaSrc = Paths.get( "A.java" );

Files.write( javaSrc,
Collections.singleton( "class A { static { ¿
System.out.println(\"Java Compiler API\"); } }" ),
StandardCharsets.UTF_8 );

Im nächsten Schritt ist die Compiler-API gefragt:

Listing 18.2com/tutego/insel/tools/CompileDemo.java, main() Teil 2

try ( StandardJavaFileManager fm = compiler.getStandardFileManager(
null /*DiagnosticListener*/, null /*Locale*/, null /*Charset*/ ) ) {
Iterable<? extends JavaFileObject> files =
fm.getJavaFileObjectsFromStrings( Collections.singleton( javaSrc.toString() ) );
CompilationTask task = compiler.getTask(
null /*Writer*/, fm, null /*DiagnosticListener*/, null /*Iterable<String>*/,
null /*Iterable<String>*/, files );
task.call();
}

Folgende Typen spielen mit:

  • javax.tools.ToolProvider bietet Einstieg zu den JDK-Dienstprogrammen. Bisher können Entwickler zum Compiler über getSystemJavaCompiler() und zum Javadoc-Tool über getSystemDocumentationTool() Zugang bekommen; das Dokumentationstool wird in Kapitel 23, »Dienstprogramme für die Java-Umgebung«, besprochen. Die statische Methode getSystemJavaCompiler() liefert ein Objekt vom Typ JavaCompiler zurück oder null, wenn das JRE installiert ist, aber nicht das JDK bzw. wenn tools.jar nicht im Klassenpfad ist.

  • Vor der Übersetzung muss der Compiler initialisiert werden, etwa mit dem Ursprung der zu übersetzenden Quellen. Dazu wird dem Compiler bei getTask(…) ein Objekt vom Typ JavaFileManager mitgegeben. Vom JavaCompiler lässt sich über getStandardFileManager(…) ein StandardJavaFileManager holen, der dateiorientiert arbeitet. Es lassen sich eigene JavaFileManager entwickeln – das werden wir später machen, wenn die Klassen aus Bytefeldern stammen.

  • Über den JavaFileManager weiß der Compiler, wo er die Quellen zu suchen hat und wohin er die Bytecode-Dateien schreiben soll. Doch weiß er nicht, welche Klassen er genau übersetzen soll. Dazu beschreibt JavaFileObject die zu übersetzende Kompilationseinheit. Den Aufbau dieser JavaFileObject-Exemplare übernimmt in unserem Fall getJavaFileObjectsFromStrings(Iterable<String> names), das eine Sammlung von Dateinamen in JavaFileObject-Typen konvertiert. Die Alternative nimmt File-Objekte statt Strings mit Pfadnamen und heißt getJavaFileObjectsFromFiles(Iterable<? extends File> files), passt also File-Objekte in die gewünschten JavaFileObject-Typen an.

  • Mit dem JavaFileManager und der Liste der Kompilationseinheiten 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 oft als Argument 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 Compileroptionen übergeben, also Pfade und sonstige Einstellungen:

Iterable<String> options = Arrays.asList( "-verbose" );
CompilationTask task = compiler.getTask( null, fm, null, options, null, files );

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 18.3com/tutego/insel/tools/CompileDemo.java, main() Teil 3

URL[] url = { javaSrc.toAbsolutePath().getParent().toUri().toURL() };
try ( URLClassLoader classLoader = new URLClassLoader( url ) ) {
Class.forName( "A", true, classLoader ); // Java Compiler API
}

Files.delete( javaSrc );

[»]Hinweis

Der Typ CompilationTask ist methodenarm. Neben der call()-Methode gibt es gerade einmal setLocale(Locale) zum Setzen der Sprache für Fehler- und Diagnosemeldungen sowie setProcessors(Iterable<? extends Processor> processors). Die letzte Methode ermöglicht die Zuweisung von Annotations-Prozessoren. Mit ihnen haben wir uns schon in Kapitel 17, »Typen, Reflection und Annotationen«, beschäftigt.

 
Zum Seitenanfang

18.2.2Fehlerdiagnose Zur vorigen ÜberschriftZur nächsten Ü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 18.4com/tutego/insel/tools/CompileWithDiagnosticsDemo.java, main() Ausschnitt

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics =
new DiagnosticCollector<>();
try ( StandardJavaFileManager fm = compiler.getStandardFileManager( diagnostics, null, null ) ) {
Iterable<? extends JavaFileObject> files =
fm.getJavaFileObjectsFromFiles( Arrays.asList( new File("QQ.java"), javaSrc.toFile() ) );
CompilationTask task = compiler.getTask( null, fm, diagnostics, null, null, files );
boolean success = task.call();
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 des Compilers 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 18.5com/tutego/insel/tools/ SimpleDiagnosticListener.java, SimpleDiagnosticListener

public class SimpleDiagnosticListener implements DiagnosticListener<JavaFileObject> {
@Override
public void report( Diagnostic<? extends JavaFileObject> diagnostic ) {
// ...
}
}
 
Zum Seitenanfang

18.2.3Eine im String angegebene Kompilationseinheit übersetzen Zur vorigen ÜberschriftZur nächsten Überschrift

Steht der Quellcode einer Kompilationseinheit 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 Kompilationseinheit über getCharContent(boolean) verfügbar machen:

Listing 18.6com/tutego/insel/tools/StringJavaFileObject.java, 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 Kompilationseinheit. 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> files =
fileManager.getJavaFileObjectsFromFiles( Arrays.asList( javaSrc.toFile() ) );

ersetzt durch:

Iterable<? extends JavaFileObject> files = Arrays.asList( javaFile );

Das gesamte Programm sieht so aus:

Listing 18.7com/tutego/insel/tools/CompileFromStringDemo.java, 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> files = Arrays.asList( javaFile );
CompilationTask task = compiler.getTask( null, fileManager, null, null, null, files );
task.call();
fileManager.close();

URL[] urls = new URL[] { Paths.get( "." ).toUri().toURL() };
try ( URLClassLoader classLoader = new URLClassLoader( urls ) ) {
Class.forName( "A", true, classLoader ); // Java Compiler API 2
}
 
Zum Seitenanfang

18.2.4Wenn Quelle und Ziel der Speicher sind Zur vorigen ÜberschriftZur nächsten Ü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 ein 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 18.8com/tutego/insel/tools/CompileToMemoryDemo.java, main()

String src = "public class A { static { System.out.println(\"Java Compiler API 3\"); } }";

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
MemClassLoader classLoader = new MemClassLoader();
try ( JavaFileManager fm = new MemJavaFileManager( compiler, classLoader ) ) {
JavaFileObject javaFile = new StringJavaFileObject( "A", src );
Collection<JavaFileObject> files = Collections.singleton( javaFile );
CompilationTask task = compiler.getTask( null, fm, null,
null, null, files );
task.call();
}

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(JavaFileManager.Location location, String className, JavaFileObject.Kind kind, FileObject sibling) 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(String) zugänglich.

Zu den drei Klassen:

Listing 18.9com/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 18.10com/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 18.11com/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<>();

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 );
}
}

 


Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Java SE 8 Standard-Bibliothek Java SE 8 Standard-Bibliothek
Jetzt Buch bestellen

 Buchempfehlungen
Zum Rheinwerk-Shop: Java ist auch eine Insel
Java ist auch eine Insel


Zum Rheinwerk-Shop: Professionell entwickeln mit Java EE 8
Professionell entwickeln mit Java EE 8


Zum Rheinwerk-Shop: Besser coden
Besser coden


Zum Rheinwerk-Shop: Entwurfsmuster
Entwurfsmuster


Zum Rheinwerk-Shop: IT-Projektmanagement
IT-Projektmanagement


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2018
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook 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

Cookie-Einstellungen ändern