Java’s Compiler API offers powerful capabilities for dynamic code generation, allowing developers to create and execute code at runtime. I’ve extensively worked with this API and found it invaluable for creating adaptable, flexible applications. Here’s a comprehensive look at eight essential techniques for dynamic code generation in Java.
Basic Compilation
The foundation of dynamic code generation starts with understanding how to compile Java source code at runtime. The Java Compiler API provides a straightforward way to accomplish this task.
public class RuntimeCompiler {
public Class<?> compileClass(String className, String sourceCode) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
JavaFileObject compilationUnit = new StringJavaFileObject(className, sourceCode);
compiler.getTask(null, fileManager, null, null, null,
List.of(compilationUnit)).call();
return loadClass(className);
}
private Class<?> loadClass(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Failed to load compiled class", e);
}
}
}
This approach compiles Java source code into bytecode that can be loaded by the JVM. I typically use this when I need to generate simple classes on the fly.
In-Memory Compilation
For more advanced scenarios, compiling code in memory without writing to the file system provides greater flexibility and performance.
public Class<?> compileInMemory(String className, String sourceCode) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
InMemoryFileManager fileManager = new InMemoryFileManager(
compiler.getStandardFileManager(null, null, null));
JavaFileObject source = new SourceFileObject(className, sourceCode);
CompilationTask task = compiler.getTask(null, fileManager, null, null, null, List.of(source));
boolean success = task.call();
if (success) {
return fileManager.getClassLoader().loadClass(className);
}
throw new CompilationException("Compilation failed");
}
The custom InMemoryFileManager
class retains the compiled bytecode in memory, avoiding disk I/O. This technique is particularly useful for applications that generate large amounts of code dynamically, such as template engines or domain-specific language interpreters.
Diagnostic Collection
Error handling is crucial when generating code dynamically. The Compiler API provides tools to collect and analyze compilation diagnostics.
public CompilationResult compile(String sourceCode) {
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
JavaFileObject file = new StringJavaFileObject("Example", sourceCode);
CompilationTask task = compiler.getTask(null, null, diagnostics, null, null, List.of(file));
boolean success = task.call();
return new CompilationResult(success, diagnostics.getDiagnostics());
}
This pattern allows for detailed error reporting, which is essential when debugging dynamically generated code. I’ve found this particularly helpful when implementing code generators that need to provide meaningful feedback to users.
Dynamic Method Generation
Creating individual methods dynamically can be more efficient than generating entire classes when only specific functionality is needed.
public Object invokeGeneratedMethod(String methodBody, Object... args) {
String className = "DynamicMethod" + UUID.randomUUID().toString().replace("-", "");
StringBuilder sourceBuilder = new StringBuilder();
sourceBuilder.append("public class ").append(className).append(" {");
sourceBuilder.append(" public Object execute(Object[] args) {");
sourceBuilder.append(methodBody);
sourceBuilder.append(" }");
sourceBuilder.append("}");
Class<?> clazz = compileClass(className, sourceBuilder.toString());
Object instance = clazz.getDeclaredConstructor().newInstance();
return clazz.getMethod("execute", Object[].class).invoke(instance, new Object[]{args});
}
This technique is perfect for evaluating expressions or implementing rule engines where business logic might change frequently.
Annotation Processing
The Compiler API enables powerful annotation processing capabilities, allowing for code generation based on annotated elements.
public class CustomAnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(annotation);
for (Element element : elements) {
processElement(element);
}
}
return true;
}
private void processElement(Element element) {
// Generate code based on the annotated element
String generatedCode = generateCode(element);
try {
JavaFileObject file = processingEnv.getFiler()
.createSourceFile(element.getSimpleName() + "Generated");
try (Writer writer = file.openWriter()) {
writer.write(generatedCode);
}
} catch (IOException e) {
processingEnv.getMessager().printMessage(Kind.ERROR, e.toString());
}
}
}
I’ve used annotation processors to generate boilerplate code for data access objects, REST endpoints, and serialization logic. This approach shifts code generation to compile-time, improving runtime performance.
Template-Based Code Generation
Templates provide a clean separation between the structure of generated code and the variable parts that change with each generation.
public class CodeGenerator {
private final String templateCode;
public CodeGenerator(String templateCode) {
this.templateCode = templateCode;
}
public String generateFromTemplate(Map<String, String> replacements) {
String result = templateCode;
for (Map.Entry<String, String> entry : replacements.entrySet()) {
result = result.replace("${" + entry.getKey() + "}", entry.getValue());
}
return result;
}
public Class<?> compileFromTemplate(String className, Map<String, String> replacements) {
String sourceCode = generateFromTemplate(replacements);
return compileClass(className, sourceCode);
}
}
This pattern is incredibly versatile. I often create template libraries for common patterns and then customize them at runtime based on specific requirements.
Runtime Code Evaluation
For simpler scenarios, Java provides script evaluation capabilities that can be combined with the Compiler API.
public class ScriptEvaluator {
private final ScriptEngine engine;
public ScriptEvaluator() {
engine = new ScriptEngineManager().getEngineByName("nashorn");
}
public Object evaluate(String script, Map<String, Object> variables) throws ScriptException {
Bindings bindings = engine.createBindings();
bindings.putAll(variables);
return engine.eval(script, bindings);
}
}
While the Nashorn JavaScript engine was deprecated in Java 11, this pattern remains valid with other script engines like GraalVM’s JavaScript implementation. I’ve found this approach valuable for quick expression evaluation without the overhead of full compilation.
Secured Code Execution
Security is paramount when executing dynamically generated code, especially in multi-tenant environments or when processing user input.
public class SecuredCodeExecutor {
public <T> T runWithPermissions(Class<T> interfaceClass, String sourceCode,
Permission... permissions) {
SecurityManager originalManager = System.getSecurityManager();
try {
Policy.setPolicy(createRestrictivePolicy(permissions));
System.setSecurityManager(new SecurityManager());
Class<?> compiledClass = compileClass("SecuredImpl", sourceCode);
return interfaceClass.cast(compiledClass.getDeclaredConstructor().newInstance());
} finally {
System.setSecurityManager(originalManager);
}
}
private Policy createRestrictivePolicy(Permission... permissions) {
return new Policy() {
@Override
public PermissionCollection getPermissions(CodeSource codesource) {
Permissions perms = new Permissions();
for (Permission permission : permissions) {
perms.add(permission);
}
return perms;
}
};
}
}
This technique creates a sandbox for executing dynamically generated code with limited permissions. I’ve implemented this pattern in systems where users can define custom business rules or data transformations without compromising system security.
Advanced Implementation Considerations
When implementing dynamic code generation in production systems, several additional factors deserve consideration. Performance optimization is crucial, especially in high-throughput applications. Caching compiled classes can significantly reduce overhead. I typically implement a two-level cache: one for source code to avoid regenerating identical code, and another for compiled classes to avoid recompilation.
public class CachingCompiler {
private final Map<String, String> sourceCache = new ConcurrentHashMap<>();
private final Map<String, Class<?>> classCache = new ConcurrentHashMap<>();
public Class<?> getOrCompile(String key, Function<String, String> sourceGenerator) {
return classCache.computeIfAbsent(key, k -> {
String source = sourceCache.computeIfAbsent(k, sourceGenerator);
return compileClass("Generated_" + k.hashCode(), source);
});
}
}
Another important consideration is class loading and unloading. The default class loader retains references to loaded classes, potentially causing memory leaks in long-running applications that generate many classes. Custom class loaders can address this issue.
public class DisposableClassLoader extends ClassLoader {
private final Map<String, byte[]> classDefinitions = new HashMap<>();
public void addClassDefinition(String name, byte[] bytecode) {
classDefinitions.put(name, bytecode);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytecode = classDefinitions.get(name);
if (bytecode == null) {
throw new ClassNotFoundException(name);
}
return defineClass(name, bytecode, 0, bytecode.length);
}
}
For complex code generation tasks, I recommend using a proper abstract syntax tree (AST) representation rather than string manipulation. While the examples in this article use string building for clarity, tools like JavaPoet provide a more robust approach for serious applications.
public String generateDataClass(String className, List<FieldInfo> fields) {
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC);
for (FieldInfo field : fields) {
FieldSpec fieldSpec = FieldSpec.builder(
Class.forName(field.getType()), field.getName(), Modifier.PRIVATE)
.build();
classBuilder.addField(fieldSpec);
// Generate getter
MethodSpec getter = MethodSpec.methodBuilder("get" + capitalizeFirst(field.getName()))
.addModifiers(Modifier.PUBLIC)
.returns(Class.forName(field.getType()))
.addStatement("return this.$N", field.getName())
.build();
classBuilder.addMethod(getter);
// Generate setter
MethodSpec setter = MethodSpec.methodBuilder("set" + capitalizeFirst(field.getName()))
.addModifiers(Modifier.PUBLIC)
.addParameter(Class.forName(field.getType()), field.getName())
.addStatement("this.$1N = $1N", field.getName())
.build();
classBuilder.addMethod(setter);
}
JavaFile javaFile = JavaFile.builder("com.example.generated", classBuilder.build())
.build();
return javaFile.toString();
}
Dynamic code generation opens up remarkable possibilities in Java applications. I’ve successfully implemented these techniques in various contexts, from ORM frameworks and template engines to rule processors and domain-specific language interpreters. The ability to generate and execute code at runtime provides a level of flexibility that static compilation alone cannot match.
By mastering these eight techniques, you can enhance your applications with dynamic behavior while maintaining type safety and performance. The Compiler API might seem complex at first, but the power it grants to your applications is well worth the learning curve.