How to compile Java code at runtime

In this tutorial I will show you how you can compile Java code from within a running Java program.

The complete source code can be found on my Gitlab repo
To be able to compile Java code at runtime you will need to run your Java application on a JDK as this tutorial is based on the Java Compiler Toolkit which is not included in a JRE.

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<>();
The diagnostics collector will collect all error messages that the compiler reports while the compilation

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();
    }
}
You code now calls the Java compiler to compile source code provided as a String.
You can now use Reflection to define and use the classes that you have compiled from your code. You could also use the PluginClassLoader for this which is provided by the Java Plugin Framework.

Related articles: