skip to content
Honwhy Wang

Java ASM & Java Poet

/ 3 min read

As the name implies, writing poetry in Java.

The question of “how to programe language X in language X” can be quite fascinating. I have hands-on experience generating Java code (including bytecode) within Java, generating Groovy code within Groovy, and I’ve also dabbled in Rust’s macro system (albeit only at the “Hello World” level).

Generating Bytecode with Java

Experienced Java developers who generate bytecode often reach for libraries such as Bytebuddy or ASM. For example, to produce a simple marker interface:

public interface AtVersion001 {}

you could use ASM like this:

// Create the ClassWriter
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// Define the class: Java 8, public interface, extending Object
cw.visit(
Opcodes.V1_8,
Opcodes.ACC_PUBLIC + Opcodes.ACC_INTERFACE,
fullClassName.replace(".", "/"),
null,
"java/lang/Object",
null
);
// Finish the class definition
cw.visitEnd();

Of course, this bytecode must be placed in the correct output directory (/target/classes) for your application to load it. To achieve that in the context of annotation processing, you leverage the JSR-269 API. For demonstration purposes, define a custom annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@Repeatable(AtVersions.class)
public @interface AtVersion {
String value();
}

and implement an annotation processor:

@SupportedAnnotationTypes("com.honwhy.examples.common.annotation.AtVersions")
public class AtVersionProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
// Log for demonstration
System.out.println("AtVersionProcessor invoked");
// Find all elements annotated with @AtVersions
Set<? extends Element> elements =
roundEnv.getElementsAnnotatedWith(AtVersions.class);
elements.forEach(this::generateCode);
return false;
}
}

The key to writing the generated bytecode out to /target/classes is using the Filer’s createClassFile method, which resolves the proper path:

JavaFileObject classFile =
processingEnv.getFiler().createClassFile(fullClassName);
try (OutputStream os = classFile.openOutputStream()) {
os.write(cw.toByteArray());
os.flush();
}

Generating Java Source Code with Java

By the same principle, you can emit Java source files from within an annotation processor. A straightforward (if verbose) approach might look like this:

AtVersion[] annotations = element.getAnnotationsByType(AtVersion.class);
for (AtVersion atVersion : annotations) {
String className = "AtVersion" + atVersion.value();
String packageName = getPackageName(element);
StringBuilder sb = new StringBuilder()
.append("package ").append(packageName).append(";\n")
.append("public interface ").append(className).append(" {}\n");
// …write sb.toString() out as a .java file
}

However, this generated source must be placed in the correct directory (/target/generated-sources/annotations) to be picked up by the compiler. To write resources there, use:

FileObject sourceFile =
processingEnv.getFiler().createResource(
StandardLocation.SOURCE_OUTPUT,
packageName,
className + ".java"
);
try (Writer writer = sourceFile.openWriter()) {
writer.write(sb.toString());
writer.flush();
}

A More Elegant Approach with JavaPoet

Is there a more elegant way? Developers familiar with Lombok and MapStruct know that these libraries streamline source-code generation. For example, asking a tool like DeepSeek:

Please analyze the source code of MapStruct’s org.mapstruct.ap.MappingProcessor class and explain how it generates Java source code.

reveals that MapStruct uses [JavaPoet], the “poetic” API for programmatic Java code construction. Rewriting the manual builder above in JavaPoet:

TypeSpec atInterface = TypeSpec
.interfaceBuilder(className)
.addModifiers(Modifier.PUBLIC)
.build();
JavaFile javaFile = JavaFile
.builder(packageName, atInterface)
.indent(" ")
.build();

Then emitting it is as simple as:

FileObject sourceFile =
processingEnv.getFiler().createResource(
StandardLocation.SOURCE_OUTPUT,
packageName,
className + ".java"
);
try (Writer writer = sourceFile.openWriter()) {
writer.write(javaFile.toString()); // ← single-line change
writer.flush();
}

Key Dependencies

<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.7.1</version>
</dependency>
<dependency>
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
<version>1.10.0</version>
</dependency>

Inspiration