For most of its life, the Java compilation process has been a distinct, offline step: you write .java files, you run the javac compiler, and you get .class files. But what if you could do this dynamically, at runtime, taking source code from a string or an input stream and generating executable bytecode on the fly?

Since Java 6, you can. This capability is provided by the Java Compiler API, specified by JSR 199. It gives developers full programmatic access to the javac compiler without needing to start a separate OS process.

Why Compile Java at Runtime?

This isn’t just a novelty; it’s the engine behind many powerful Java tools and frameworks:

  • Template Engines: JSP servers (like Tomcat) and modern template engines compile .jsp files or templates into Java servlets for maximum performance.
  • Interactive Shells: The jshell tool introduced in Java 9 is a prime example. It reads a line of code, compiles it in memory, executes it, and shows you the result.
  • Scripting within Applications: You can allow users to write custom logic in Java that your application can then compile and integrate.
  • Proxy Generation: Frameworks that generate dynamic proxies can create the source code for the proxy class and compile it instantly.
  • Live Coding Environments: Tools that allow for hot-reloading of code often use this API to recompile a class in the background.

The Core Components of the API

The API, located in the javax.tools package, revolves around a few key interfaces.

graph TD A["Your Application"] --> B("ToolProvider.getSystemJavaCompiler()") B --> C{"JavaCompiler"}; subgraph "Compilation Task" C -- invokes --> D("getTask"); D -- uses --> E("JavaFileManager"); D -- uses --> F("Iterable JavaFileObject"); end E -- manages --> G["Input: .java source"]; E -- manages --> H["Output: .class bytecode"]; F -- contains --> G; D -- produces --> H; A --> I("Custom ClassLoader"); H -- loaded by --> I; I --> J["Runnable Class"];
  1. JavaCompiler: This is the main interface representing the compiler itself. You get an instance of it from ToolProvider.getSystemJavaCompiler(). . JavaFileObject: An abstraction for a “file” of code. Crucially, this doesn’t have to be a physical file on disk. It can be a wrapper around a String in memory.
  2. JavaFileManager: Manages how the compiler finds source files and, more importantly for us, where it writes the output bytecode. We can customize this to write the .class data to a byte array in memory instead of to the file system.
  3. CompilationTask: An object representing a single compilation job, which you then execute.

A Practical Example: Compiling a String to a Class

Let’s walk through a complete example. Our goal is to take a Java class defined in a String, compile it, load it, and execute a method on it—all without creating any physical files.

The Source Code (as a String)

// Our dynamic source code
String sourceCode = """
    public class DynamicGreeter {
        public void sayHello() {
            System.out.println("Hello from a dynamically compiled class!");
        }
    }
""";

The Full Compilation and Execution Logic

import javax.tools.*;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class InMemoryCompiler {

    public static void main(String[] args) throws Exception {
        // 1. Define the source code
        String className = "DynamicGreeter";
        String sourceCode = "public class " + className + " {\n"
                        + "    public void sayHello() {\n"
                        + "        System.out.println(\"Hello from a dynamically compiled class!\");\n"
                        + "    }\n"
                        + "}\n";

        // 2. Get an instance of the system Java compiler
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        if (compiler == null) {
            System.err.println("Compiler not available. Ensure you are running on a JDK, not a JRE.");
            return;
        }

        // 3. Create a in-memory file object for the source code
        JavaFileObject sourceFile = new StringSourceFileObject(className, sourceCode);

        // 4. Create a custom file manager to write bytecode to memory
        InMemoryFileManager fileManager = new InMemoryFileManager(compiler.getStandardFileManager(null, null, null));

        // 5. Create and run the compilation task
        JavaCompiler.CompilationTask task = compiler.getTask(
                null, fileManager, null, null, null, Collections.singletonList(sourceFile));

        boolean success = task.call();
        System.out.println("Compilation successful: " + success);

        if (success) {
            // 6. Create a custom class loader to load our in-memory class
            InMemoryClassLoader classLoader = new InMemoryClassLoader(fileManager.getBytecode());
            
            // 7. Load, instantiate, and invoke the method
            Class<?> greeterClass = classLoader.loadClass(className);
            Object greeterInstance = greeterClass.getConstructor().newInstance();
            Method sayHelloMethod = greeterClass.getMethod("sayHello");
            sayHelloMethod.invoke(greeterInstance);
        }
    }
}

// Helper class: Represents a Java source file held in a String.
class StringSourceFileObject extends SimpleJavaFileObject {
    private final String code;
    public StringSourceFileObject(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; }
}

// Helper class: A file manager that stores compiled bytecode in a map.
class InMemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
    private final Map<String, ByteArrayOutputStream> bytecode = new HashMap<>();
    protected InMemoryFileManager(JavaFileManager fileManager) { super(fileManager); }
    public Map<String, byte[]> getBytecode() {
        Map<String, byte[]> result = new HashMap<>();
        bytecode.forEach((k, v) -> result.put(k, v.toByteArray()));
        return result;
    }
    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) {
        if (kind == JavaFileObject.Kind.CLASS) {
            return new SimpleJavaFileObject(URI.create("mem:///" + className), kind) {
                @Override public OutputStream openOutputStream() {
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    bytecode.put(className, baos);
                    return baos;
                }
            };
        }
        return super.getJavaFileForOutput(location, className, kind, sibling);
    }
}

// Helper class: A class loader that can load a class from a byte array.
class InMemoryClassLoader extends ClassLoader {
    private final Map<String, byte[]> bytecode;
    public InMemoryClassLoader(Map<String, byte[]> bytecode) { this.bytecode = bytecode; }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] bytes = bytecode.get(name);
        if (bytes == null) { throw new ClassNotFoundException(name); }
        return defineClass(name, bytes, 0, bytes.length);
    }
}
public class DynamicGreeter {
    public void sayHello() {
        System.out.println("Hello from a dynamically compiled class!");
    }
}

// How to use it:
Class<?> greeterClass = classLoader.loadClass("DynamicGreeter");
Object greeterInstance = greeterClass.getConstructor().newInstance();
Method sayHelloMethod = greeterClass.getMethod("sayHello");
sayHelloMethod.invoke(greeterInstance);

Important Gotcha: JDK vs. JRE

The ToolProvider.getSystemJavaCompiler() method will only return a compiler instance if the application is running on a full Java Development Kit (JDK). If you run the same code on a Java Runtime Environment (JRE), it will return null, because the JRE does not include tools.jar (or its modular equivalent), which contains the compiler implementation.

This API is a testament to the power and flexibility of the JVM ecosystem, enabling a level of dynamism that unlocks sophisticated tools and architectures.