Java compile in Java
In a previous post I wrote about how to generate a proxy during run-time and we got as far as having Java source code generated. However to use the class it has to be compiled and the generated byte code to be loaded into memory. That is "compile" time. Luckily since Java 1.6 we have access the Java compiler during run time and we can, thus mix up compile time into run time. Though that may lead a plethora of awful things generally resulting unmaintainable self modifying code in this very special case it may be useful: we can compile our run-time generated proxy.
1. Java compiler API
The Java compiler reads source files and generates class files. (Assembling them to JAR, WAR, EAR and other packages is the responsibility of a different tool.) The source files and class files do not necessarily need to be real operating system files residing in a magnetic disk, SSD or memory drive. After all Java is usually good about abstraction when it comes to the run-time API and this is the case now. These files are some "abstract" files you have to provide access to via an API that can be disk files but the same time they can be almost anything else. It would generally be a waste of resources to save the source code to disk just to let the compiler running in the same process to read it back and to do the same with the class files when they are ready.
The Java compiler as an API available in the run-time requires that you provide some simple API (or SPI of you like the term) to access the source code and also to send the generated byte code. In case we have the code in memory we can have the following code (from this file):
public Class<?> compile(String sourceCode, String canonicalClassName)
throws Exception {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
List<JavaSourceFromString> sources = new LinkedList<>();
String className = calculateSimpleClassName(canonicalClassName);
sources.add(new JavaSourceFromString(className, sourceCode));
StringWriter sw = new StringWriter();
MemoryJavaFileManager fm = new MemoryJavaFileManager(
compiler.getStandardFileManager(null, null, null));
JavaCompiler.CompilationTask task = compiler.getTask(sw, fm, null,
null, null, sources);
Boolean compilationWasSuccessful = task.call();
if (compilationWasSuccessful) {
ByteClassLoader byteClassLoader = new ByteClassLoader(new URL[0],
classLoader, classesByteArraysMap(fm));
Class<?> klass = byteClassLoader.loadClass(canonicalClassName);
byteClassLoader.close();
return klass;
} else {
compilerErrorOutput = sw.toString();
return null;
}
}
This code is part of the opensource project Java Source Code Compiler (jscc) and it is in the file Compiler.java.
The compiler instance is available through the ToolProvider
and to create a compilation task we have to invoke getTask()
. The code write the errors into a string via a string writer. The file manager (fm
) is implemented in the same package and it simply stored the files as byte arrays in a map, where the keys are the "file names". This is where the class loader will get the bytes later when the class(es) are loaded. The code does not provide any diagnistic listener (see the documentation of the java compiler in the RT), compiler options or classes to be processed by annotation processors. These are all nulls. The last argument is the list of source codes to compile. We compile only one single class in this tool, but since the compiler API is general and expects an iterable source we provide a list. Since there is another level of abstraction this list contains `JavaSourceFromString`s.
To start the compilation the created task has to be "call"ed and if the compilation was successful the class is loaded from the generated byte array or arrays. Note that in case there is a nested or inner class inside the top level class we compile then the compiler will create several classes. This is the reason we have to maintain a whole map for the classes and not a single byte array even though we compile only one source class. If the compilation was not successful then the error output is stored in a field and can be queried.
The use of the class is very simple and you can find samples in the unit tests:
private String loadJavaSource(String name) throws IOException {
InputStream is = this.getClass().getResourceAsStream(name);
byte[] buf = new byte[3000];
int len = is.read(buf);
is.close();
return new String(buf, 0, len, "utf-8");
}
...
@Test
public void given_PerfectSourceCodeWithSubClasses_when_CallingCompiler_then_ProperClassIsReturned()
throws Exception {
final String source = loadJavaSource("Test3.java");
Compiler compiler = new Compiler();
Class<?> newClass = compiler.compile(source, "com.javax0.jscc.Test3");
Object object = newClass.newInstance();
Method f = newClass.getMethod("method");
int i = (int) f.invoke(object, null);
Assert.assertEquals(1, i);
}
Comments
Please leave your comments using Disqus, or just press one of the happy faces. If for any reason you do not want to leave a comment here, you can still create a Github ticket.