Programme mit der Compiler API übersetzen

Zum Übersetzen von Java-Programmen definiert Java 6 eine standardisierte API über das Paket javax.tools. Vor Java 6 mussten Entwickler entweder direkt den Java-Compiler von Sun ansprechen oder über eine externe Bibliothek wie den Apache Commons Java Compiler Interface (JCI) unter http://tutego.com/go/jci einen Compiler, etwa den von Eclipse, ansprechen.

Java Compiler API

Wir wollen mit dem Java Compiler API aus Java 6 ein Programm entwickeln, das Java-Quellcode auf die Festplatte schreibt, diesen dann übersetzt und anschließend lädt. Zum ersten Teil:

Writer p = new FileWriter( "c:/A.java" );
p.write( "class A { static { System.out.println(\"Java Compiler API\"); } }" );
p.close();

Der FileWriter schreibt ein einfaches Java-Programm auf das Laufwerk C:/. Der Compiler kann das Programm nun übersetzen, wobei er natürlich auch das Programm als Eingabestrom empfangen kann.

JavaCompiler tool = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager manager = tool.getStandardFileManager( null, null, null );
List<File> fileList = Arrays.asList( new File("c:/A.java") );
Iterable<? extends JavaFileObject> units;
units = manager.getJavaFileObjectsFromFiles( fileList );
CompilationTask task = tool.getTask( null, manager, null, null, null, units );
task.call();
manager.close();

Der ToolProvider gibt mit der statischen Funktion ein Objekt vom Typ JavaCompiler zurück, das den Compiler repräsentiert. Das Objekt steuert die Übersetzung und die Fehlermeldungen. Mit dem JavaCompiler sind ein optionaler DiagnosticListener verbunden und ein StandardJavaFileManager, den getStandardFileManager() liefert. Er bestimmt mit getJavaFileObjectsFromFiles() oder getJavaFileObjectsFromStrings() die Eingabedateien als Liste von JavaFileObject-Objekten. Mit dem File-Manager und der Liste der JavaFileObject-Objekte liefert das JavaCompilerTool-Objekt anschließend mit getTask() ein JavaCompilerTool.CompilationTask-Objekt, das call() anbietet, um die Übersetzung zu starten.

Im letzten Schritt kann ein eigener Klassenlader die Klasse laden, und es folgt eine Ausgabe auf dem Bildschirm:

URLClassLoader classLoader = new URLClassLoader(
  new URL[] { new File( "c:/" ).toURI().toURL() } );
Class.forName( "A", true, classLoader ); // Java Compiler API

Im String angegebene Klasse übersetzen

Zum Übersetzen von Strings muss das Programm nicht erst eine temporäre Datei schreiben. Zwar sieht die Compiler-API nicht direkt eine einfache Funktion zum Übersetzten von Klassen aus Strings vor, doch schwierig ist es auch nicht. Der erste Schritt ist die Entwicklung einer eigenen JavaFileObject-Klasse.

class JavaSourceFromString extends SimpleJavaFileObject
{
  private final String code;

  public JavaSourceFromString( String name, String 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 einen Namen der Klasse und den String mit der Klassendefinition.

Jetzt können wir fast so vorgehen wie im letzten Beispiel. Erst bauen wir das JavaFileObject auf, bauen für getTask() ein Iterable<? extends JavaFileObject> mit diesem einen Objekt und rufen call() auf.

String src = "class B { static { System.out.println(\"Aus der Ursuppe\"); } }";
JavaFileObject javaFile = new JavaSourceFromString( "B", src );
units = Arrays.asList( javaFile ); 
task = tool.getTask( null, manager, null, null, null, units );
task.call();

Das übersetzt die Klasse und ermöglicht später das Laden:

Class.forName( "B" ); // Aus der Ursuppe

Kompletter Quellcode für das Beispiel

package com.tutego.insel.tools;

import java.io.File;
import java.io.FileWriter;
import java.io.Writer;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.List;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import javax.tools.JavaCompiler.CompilationTask;

public class CompileDemo
{
  public static void main( String[] args ) throws Exception
  {
    Writer p = new FileWriter( "c:/A.java" );
    p.write( "class A { static { System.out.println(\"Java Compiler API\"); } }" );
    p.close();

    //

    JavaCompiler tool = ToolProvider.getSystemJavaCompiler();
    StandardJavaFileManager manager = tool.getStandardFileManager( null, null, null );

    List<File> fileList = Arrays.asList( new File("c:/A.java") );
    Iterable<? extends JavaFileObject> units;
    units = manager.getJavaFileObjectsFromFiles( fileList );

    CompilationTask task = tool.getTask( null, manager, null, null, null, units );
    task.call();

    String src = "class B { static { System.out.println(\"Aus der Ursuppe\"); } }";
    JavaFileObject javaFile = new JavaSourceFromString( "B", src );
    units = Arrays.asList( javaFile ); 
    task = tool.getTask( null, manager, null, null, null, units );
    task.call();

    manager.close();

    //

    URLClassLoader classLoader = new URLClassLoader(
                                  new URL[] { new File( "c:/" ).toURI().toURL() } );
    Class.forName( "A", true, classLoader ); // Java Compiler API
    Class.forName( "B" );                    // Aus der Ursuppe
  }
}

class JavaSourceFromString extends SimpleJavaFileObject
{
  private final String code;

  public JavaSourceFromString( String name, String code )
  {
    super( URI.create( "string:///" + name.replace( '.', '/' )
                       + Kind.SOURCE.extension ), Kind.SOURCE );
    this.code = code;
  }

  @Override
  public CharSequence getCharContent( boolean ignoreEncodingErrors )
  {
    return code;
  }
}