Integrating Java compiler in my application
Categories:
Integrating the Java Compiler (Javac) into Your Application

Learn how to programmatically invoke the Java compiler (javac) from within your Java applications, enabling dynamic code compilation and execution.
Integrating the Java compiler (javac
) directly into your application can unlock powerful capabilities, such as dynamic code generation, on-the-fly compilation of user-defined scripts, or even building custom IDE-like features. While most Java development involves compiling code offline using build tools like Maven or Gradle, the Java Development Kit (JDK) provides APIs to invoke the compiler programmatically. This article will guide you through the process of embedding javac
into your Java application, covering the necessary APIs and best practices.
Understanding the Java Compiler API
The core of programmatic compilation in Java lies within the javax.tools
package, specifically the JavaCompiler
interface. This interface provides a standard way to interact with the Java compiler. You can obtain an instance of the system's default Java compiler using the ToolProvider.getSystemJavaCompiler()
method. Once you have a JavaCompiler
instance, you can invoke its run()
method, passing in streams for input, output, and error, along with compilation arguments and source files.
flowchart TD A[Application Start] --> B{Get JavaCompiler Instance} B --> C["ToolProvider.getSystemJavaCompiler()"] C --> D{Prepare Compilation Arguments} D --> E["Source Files, Classpath, Output Dir"] E --> F{Invoke Compiler} F --> G["compiler.run(in, out, err, args...)"] G --> H{Check Compilation Result} H -- Success --> I[Load & Execute Compiled Class] H -- Failure --> J[Handle Compilation Errors]
Flowchart of programmatic Java compilation within an application.
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Arrays;
public class DynamicCompiler {
public static void main(String[] args) {
// 1. Get the system Java compiler
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
System.err.println("JDK not found. Please run with a JDK, not a JRE.");
return;
}
// 2. Prepare source code (e.g., from a String or file)
String sourceCode = "public class MyDynamicClass {\n" +
" public String getMessage() {\n" +
" return \"Hello from dynamically compiled code!\";\n" +
" }\n" +
"}";
// For simplicity, we'll write it to a temporary file
// In a real application, you might use a custom JavaFileObject
// to compile directly from memory.
java.io.File sourceFile = new java.io.File("MyDynamicClass.java");
try (java.io.FileWriter writer = new java.io.FileWriter(sourceFile)) {
writer.write(sourceCode);
} catch (java.io.IOException e) {
e.printStackTrace();
return;
}
// 3. Prepare compilation arguments
// -d specifies the output directory for compiled classes
String outputDir = "."; // Compile to current directory
Iterable<String> options = Arrays.asList("-d", outputDir);
Iterable<String> compilationUnits = Arrays.asList(sourceFile.getPath());
// 4. Capture compiler output/errors
Writer output = new StringWriter();
Writer errorOutput = new StringWriter();
// 5. Invoke the compiler
System.out.println("Compiling MyDynamicClass.java...");
int compilationResult = compiler.run(null, output, errorOutput, options, compilationUnits);
// 6. Check result and print output
if (compilationResult == 0) {
System.out.println("Compilation successful!");
System.out.println("Compiler Output:\n" + output.toString());
// Clean up source file
sourceFile.delete();
// Now, you can load and use the compiled class
try {
Class<?> dynamicClass = Class.forName("MyDynamicClass");
Object instance = dynamicClass.getDeclaredConstructor().newInstance();
java.lang.reflect.Method method = dynamicClass.getMethod("getMessage");
String message = (String) method.invoke(instance);
System.out.println("Invoked dynamic method: " + message);
} catch (Exception e) {
System.err.println("Error loading or invoking dynamic class: " + e.getMessage());
e.printStackTrace();
}
} else {
System.err.println("Compilation failed with exit code: " + compilationResult);
System.err.println("Compiler Error Output:\n" + errorOutput.toString());
// Clean up source file even on failure
sourceFile.delete();
}
}
}
Example of dynamically compiling and executing Java code.
Handling Source Code and Class Loading
When compiling code dynamically, you have several options for providing the source. You can write source code to temporary files, as shown in the example, or more efficiently, you can implement custom JavaFileObject
and JavaFileManager
classes to compile directly from String
objects or other in-memory sources. This avoids disk I/O and can be faster for frequent compilations.
After successful compilation, the .class
files are generated. To use these classes, you'll need to load them into your application's ClassLoader
. The simplest approach is to ensure the compiled classes are in a directory that's already on your application's classpath. For more advanced scenarios, especially when compiling multiple times or from memory, you might need to create a custom URLClassLoader
or a specialized InMemoryClassLoader
to load the generated bytecode.
javax.tools.SimpleJavaFileObject
and ForwardingJavaFileManager
. These allow you to provide source code as String
s and capture compiled bytecode as byte[]
arrays, which can then be loaded by a custom ClassLoader
.Advanced Compilation Options and Error Handling
The JavaCompiler.run()
method accepts an Iterable<String>
for compiler options, allowing you to specify various javac
command-line arguments. Common options include -classpath
(to specify dependencies for the compiled code), -source
and -target
(for Java version compatibility), and -g
(to include debugging information). Always capture the compiler's output and error streams to provide meaningful feedback to the user or for debugging purposes.
Error handling is crucial. A non-zero return code from compiler.run()
indicates compilation failure. The detailed error messages will be written to the error stream you provide. Parsing these messages can help pinpoint syntax errors, missing imports, or other issues in the dynamically generated code.
import javax.tools.*;
import java.io.*;
import java.net.URI;
import java.util.*;
// Custom JavaFileObject to hold source code in memory
class StringJavaFileObject extends SimpleJavaFileObject {
private final String code;
protected StringJavaFileObject(String name, String code) {
super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return code;
}
}
// Custom JavaFileManager to capture compiled bytecode in memory
class InMemoryJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
private final Map<String, ByteArrayOutputStream> compiledClasses = new HashMap<>();
protected InMemoryJavaFileManager(JavaFileManager fileManager) {
super(fileManager);
}
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
if (kind == JavaFileObject.Kind.CLASS) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
compiledClasses.put(className, bos);
return new SimpleJavaFileObject(URI.create("mem://" + className.replace('.', '/') + kind.extension), kind) {
@Override
public OutputStream openOutputStream() throws IOException {
return bos;
}
};
}
return super.getJavaFileForOutput(location, className, kind, sibling);
}
public byte[] getCompiledClass(String className) {
ByteArrayOutputStream bos = compiledClasses.get(className);
return bos != null ? bos.toByteArray() : null;
}
}
// Custom ClassLoader to load bytecode from memory
class InMemoryClassLoader extends ClassLoader {
private final Map<String, byte[]> classBytes;
public InMemoryClassLoader(ClassLoader parent, Map<String, byte[]> classBytes) {
super(parent);
this.classBytes = classBytes;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = classBytes.get(name);
if (bytes == null) {
return super.findClass(name);
}
return defineClass(name, bytes, 0, bytes.length);
}
}
public class DynamicCompilerInMemory {
public static void main(String[] args) throws Exception {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
System.err.println("JDK not found. Please run with a JDK, not a JRE.");
return;
}
String className = "MyInMemoryClass";
String sourceCode = "public class " + className + " {\n" +
" public String getMessage() {\n" +
" return \"Hello from in-memory compiled code!\";\n" +
" }\n" +
"}";
JavaFileObject sourceFile = new StringJavaFileObject(className, sourceCode);
List<JavaFileObject> compilationUnits = Collections.singletonList(sourceFile);
StringWriter output = new StringWriter();
StringWriter errorOutput = new StringWriter();
InMemoryJavaFileManager fileManager = new InMemoryJavaFileManager(compiler.getStandardFileManager(null, null, null));
JavaCompiler.CompilationTask task = compiler.getTask(
output,
fileManager,
null, // DiagnosticListener
null, // Options
null, // Classes for annotation processing
compilationUnits
);
System.out.println("Compiling " + className + " in memory...");
boolean success = task.call();
if (success) {
System.out.println("Compilation successful!");
System.out.println("Compiler Output:\n" + output.toString());
byte[] classBytes = fileManager.getCompiledClass(className);
if (classBytes != null) {
Map<String, byte[]> compiledClassesMap = new HashMap<>();
compiledClassesMap.put(className, classBytes);
InMemoryClassLoader classLoader = new InMemoryClassLoader(DynamicCompilerInMemory.class.getClassLoader(), compiledClassesMap);
Class<?> dynamicClass = classLoader.loadClass(className);
Object instance = dynamicClass.getDeclaredConstructor().newInstance();
java.lang.reflect.Method method = dynamicClass.getMethod("getMessage");
String message = (String) method.invoke(instance);
System.out.println("Invoked dynamic method: " + message);
} else {
System.err.println("Error: Compiled class bytes not found.");
}
} else {
System.err.println("Compilation failed.");
System.err.println("Compiler Error Output:\n" + errorOutput.toString());
}
}
}
Advanced example: Compiling Java code from a String and loading it from memory.
1. Step 1: Obtain JavaCompiler Instance
Use ToolProvider.getSystemJavaCompiler()
to get the compiler. Ensure your application is running on a JDK, not just a JRE, as the compiler is part of the JDK.
2. Step 2: Prepare Source Code
Decide whether to write source code to temporary files or use custom JavaFileObject
implementations to compile directly from memory (e.g., String
s). For simple cases, temporary files are easier.
3. Step 3: Define Compilation Arguments
Create a list of String
s for compiler options (e.g., output directory -d
, classpath -classpath
). These are the same arguments you'd pass to javac
from the command line.
4. Step 4: Invoke the Compiler
Call compiler.run(InputStream in, OutputStream out, OutputStream err, String... arguments)
. Provide StringWriter
instances for out
and err
to capture compiler messages.
5. Step 5: Handle Results and Load Classes
Check the integer return code (0 for success). If successful, load the compiled .class
files using a ClassLoader
(e.g., URLClassLoader
for file-based compilation or a custom InMemoryClassLoader
for in-memory compilation).