In this tutorial I will show you how you can compile Java code from within a running Java program.
Create an own FileManager
To get maximum performance, the compilation shall be done completely in the system memory. For this we need to create an own FileManager that is to be used by the compiler and relays the write operation for the compiled byte code into a JavaFileObject that is provided by the caller:
public class CompilerFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
private final JavaFileObject javaFileObject;
public CompilerFileManager(StandardJavaFileManager fileManager, JavaFileObject javaFileObject) {
super(fileManager);
this.javaFileObject = javaFileObject;
}
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
return javaFileObject;
}
}
Create necessary wrapper classes
As the Compiler Toolkit internally works with JavaFileObjects we need to create some wrapper classes.
The first class will be called JavaByteObject and relays the compiled byte code into a byte array:
public class JavaByteObject extends SimpleJavaFileObject {
private ByteArrayOutputStream outputStream;
public JavaByteObject(String name) {
super(URI.create(String.format("bytes:///%s%s", name, name.replaceAll("\\.", "/"))), Kind.CLASS);
}
@Override
public OutputStream openOutputStream() throws IOException {
this.outputStream = new ByteArrayOutputStream();
return outputStream;
}
public byte[] getBytes() {
return outputStream.toByteArray();
}
}
The second one is called JavaStringObject and will relay the compiler input to a String variable containing the source code to be compiled:
public class JavaStringObject extends SimpleJavaFileObject {
private final String code;
public JavaStringObject(String className, String code) {
super(URI.create(String.format(
"string:///%s%s",
className.replace('.','/'),
Kind.SOURCE.extension
)), Kind.SOURCE);
this.code = code;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}
Create custom exception
We also create a custom exception to indicate compiler errors to the user:
public class CompilerException extends Exception{
public CompilerException(String message) {
super(message);
}
}
Call the Java Compiler Toolkit
Now we run the Compiler Toolkit. Retrieve the systems Java compiler and instantiate a DiagnosticsCollector:
public class Compiler {
public static byte[] compile(String className, String sourceCode) throws CompilerException {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
Now create an instance of the JavaByteObject with the full qualified class name and provide its instance to the CompilerFileManager:
JavaByteObject javaByteObject = new JavaByteObject(className);
CompilerFileManager compilerFileManager = new CompilerFileManager(compiler.getStandardFileManager(diagnostics, null, null), javaByteObject);
Create a compilation task and provide it with a JavaStringObject that you have instantiated with the source code String to compile:
List<String> options = Collections.emptyList();
JavaCompiler.CompilationTask compilationTask = compiler.getTask(
null, compilerFileManager, diagnostics,
options, null, () -> {
JavaFileObject javaFileObject = new JavaStringObject(className, sourceCode);
return Collections.singletonList(javaFileObject).iterator();
});
Call run the compilation task:
boolean compilationSuccessful = compilationTask.call();
if (!compilationSuccessful){
String message = diagnostics.getDiagnostics().stream().map(Object::toString).collect(Collectors.joining());
throw new CompilerException(String.format("Failed to compile class '%s':\n%s", className, message));
}
return javaByteObject.getBytes();
}
}