Re: JavaCompilerTool
Folks,
Earlier in this thread, Piotr Kobzda reposted his code for compiling
on-the-fly and in-RAM (no use of disk files), updated for
Java 1.6.0 Beta 2 Build 86
I needed to take it one step further: to work incrementally, so a
class compiled in one compiler-task would be callable from a class
generated later and compiled in a separate, later task. In Piotr's
original version this only worked if the classes were compiled in
the same task.
I extended Piotr's JavaFileManager to support additional things the
compiler needed, mainly the list() method when getting the 'classPath'.
The code seems to work okay. It is posted below.
I'd be very interested in comments and criticisms about the approach
taken, & especially how likely it is to be robust as time goes on.
Thanks all (and 'specially Piotr),
Jim Goodwin
package com.mak.jcttest;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.Map.Entry;
import javax.tools.*;
import javax.tools.JavaCompilerTool.CompilationTask;
import javax.tools.JavaFileObject.Kind;
/*
* Demo of on-the-fly, all-in-RAM compilation (no disk files used).
* Based on an example by Piotr Kobzda at
*
http://groups.google.com/group/pl.comp.lang.java/msg/d37010d1cce043d0
*
* This demo modifies Piotr's code to work incrementally. Each class is
* compiled on-the-fly all-in-RAM and in its own compilation unit.
Newer
* classes can call or reference older classes.
*
* The intended application is a custom scripting language, where bits
* of script arrive one at a time and cannot be batched up. We want to
* compile each one as it arrives, and be able to use it at once. Also,
* each new bit must also be able to call any of the
previously-compiled
* bits.
*
* The demo compiles two classes, Hello1 and Hello2. Hello1 calls
* Hello2. Hello2 is compiled first, in one compiler task. Then Hello1
* is compiled, in another compiler task. Finally Hello1 is loaded and
* run.
*
* Written and debugged against Java 1.6.0 Beta 2 build 86, in Eclipse
* 3.2 Jim Goodwin July 25 2006
*/
public class OnTheFlyInRAMIncrementally {
// Source for both test classes. They go in package
// "just.generated"
public static final String SRC_Hello1 = "package just.generated;\n"
+ "public class Hello1 {\n"
+ " public static void main(String... args) {\n"
+ " System.out.println(new Hello2()); \n" + "}}\n";
public static final String SRC_Hello2 = "package just.generated;\n"
+ "public class Hello2 {\n"
+ " public String toString() {\n"
+ " return \"just hello!\";\n}}\n";
public static void main(String[] args) throws Exception {
JavaCompilerTool compiler = ToolProvider
.getSystemJavaCompilerTool();
// A map of from class names to the RAMJavaFileObject that holds
// the compiled-code for that class. This is the cache of
// compiled classes.
Map<String, JavaFileObject> output = new HashMap<String,
JavaFileObject>();
// A loader that searches our cache first.
ClassLoader loader = new RAMClassLoader(output);
DiagnosticCollector<JavaFileObject> diagnostics = new
DiagnosticCollector<JavaFileObject>();
// Create a JavaFileManager which uses our DiagnosticCollector,
// and creates a new RAMJavaFileObject for the class, and
// registers it in our cache
StandardJavaFileManager sjfm = compiler
.getStandardFileManager(diagnostics);
JavaFileManager jfm = new RAMFileManager(sjfm, output, loader);
// Create source file objects
SourceJavaFileObject src1 = new SourceJavaFileObject("Hello1",
SRC_Hello1);
SourceJavaFileObject src2 = new SourceJavaFileObject("Hello2",
SRC_Hello2);
// Compile Hello2 first. getResult() causes it to run.
CompilationTask task2 = compiler.getTask(null, jfm,
diagnostics, null, null, Arrays.asList(src2));
if (!task2.getResult()) {
for (Diagnostic dm : diagnostics.getDiagnostics())
System.err.println(dm);
throw new RuntimeException("Compilation of task 2 failed");
}
// Now compile Hello1, in its own task
CompilationTask task1 = compiler.getTask(null, jfm,
diagnostics, null, null, Arrays.asList(src1));
if (!task1.getResult()) {
for (Diagnostic dm : diagnostics.getDiagnostics())
System.err.println(dm);
throw new RuntimeException("Compilation of task 1 failed");
}
// Traces the classes now found in the cache
System.out.println("\ngenerated classes: " + output.keySet());
// Load Hello1.class out of cache, and capture the class object
Class<?> c = Class.forName("just.generated.Hello1", false,
loader);
// Run the 'main' method of the Hello class.
c.getMethod("main", String[].class).invoke(null,
new Object[] { args });
}
/*
* Help routine to convert a string to a URI.
*/
static URI toURI(String name) {
try {
return new URI(name);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
/*
* A JavaFileObject class for source code, that just uses a String for
* the source code.
*/
class SourceJavaFileObject extends SimpleJavaFileObject {
private final String classText;
SourceJavaFileObject(String className, final String classText) {
super(OnTheFlyInRAMIncrementally.toURI(className + ".java"),
Kind.SOURCE);
this.classText = classText;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors)
throws IOException, IllegalStateException,
UnsupportedOperationException {
return classText;
}
}
/*
* A JavaFileManager that presents the contents of the cache as a file
* system to the compiler. To do this, it must do four things:
*
* It remembers our special loader and returns it from getClassLoader()
*
* It maintains our cache, adding class "files" to it when the compiler
* calls getJavaFileForOutput
*
* It implements list() to add the classes in our cache to the result
* when the compiler is asking for the classPath. This is the key
trick:
* it is what makes it possible for the second compilation task to
* compile a call to a class from the first task.
*
* It implements inferBinaryName to give the right answer for cached
* classes.
*/
class RAMFileManager extends
ForwardingJavaFileManager<StandardJavaFileManager> {
private final Map<String, JavaFileObject> output;
private final ClassLoader ldr;
public RAMFileManager(StandardJavaFileManager sjfm,
Map<String, JavaFileObject> output, ClassLoader ldr) {
super(sjfm);
this.output = output;
this.ldr = ldr;
}
public JavaFileObject getJavaFileForOutput(Location location,
String name, Kind kind, FileObject sibling)
throws IOException {
JavaFileObject jfo = new RAMJavaFileObject(name, kind);
output.put(name, jfo);
return jfo;
}
public ClassLoader getClassLoader(JavaFileManager.Location
location) {
return ldr;
}
@Override
public String inferBinaryName(Location loc, JavaFileObject jfo) {
String result;
if (loc == StandardLocation.CLASS_PATH
&& jfo instanceof RAMJavaFileObject)
result = jfo.getName();
else
result = super.inferBinaryName(loc, jfo);
return result;
}
@Override
public Iterable<JavaFileObject> list(Location loc, String pkg,
Set<Kind> kind, boolean recurse) throws IOException {
Iterable<JavaFileObject> result = super.list(loc, pkg, kind,
recurse);
if (loc == StandardLocation.CLASS_PATH
&& pkg.equals("just.generated")
&& kind.contains(JavaFileObject.Kind.CLASS)) {
ArrayList<JavaFileObject> temp = new ArrayList<JavaFileObject>(
3);
for (JavaFileObject jfo : result)
temp.add(jfo);
for (Entry<String, JavaFileObject> entry : output
.entrySet()) {
temp.add(entry.getValue());
}
result = temp;
}
return result;
}
}
/**
* A JavaFileObject that uses RAM instead of disk to store the file. It
* gets written to by the compiler, and read from by the loader.
*/
class RAMJavaFileObject extends SimpleJavaFileObject {
ByteArrayOutputStream baos;
RAMJavaFileObject(String name, Kind kind) {
super(OnTheFlyInRAMIncrementally.toURI(name), kind);
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors)
throws IOException, IllegalStateException,
UnsupportedOperationException {
throw new UnsupportedOperationException();
}
@Override
public InputStream openInputStream() throws IOException,
IllegalStateException, UnsupportedOperationException {
return new ByteArrayInputStream(baos.toByteArray());
}
@Override
public OutputStream openOutputStream() throws IOException,
IllegalStateException, UnsupportedOperationException {
return baos = new ByteArrayOutputStream();
}
}
/**
* A class loader that loads what's in the cache by preference, and if
* it can't find the class there, loads from the standard parent.
*
* It is important that everything in the demo use the same loader, so
* we pass this to the JavaFileManager as well as calling it directly.
*/
final class RAMClassLoader extends ClassLoader {
private final Map<String, JavaFileObject> output;
RAMClassLoader(Map<String, JavaFileObject> output) {
this.output = output;
}
@Override
protected Class<?> findClass(String name)
throws ClassNotFoundException {
JavaFileObject jfo = output.get(name);
if (jfo != null) {
byte[] bytes = ((RAMJavaFileObject) jfo).baos.toByteArray();
return defineClass(name, bytes, 0, bytes.length);
}
return super.findClass(name);
}
}