This content originally appeared on Level Up Coding - Medium and was authored by Aditya Solge
Annotations are a powerful tool provided by Java, on which frameworks like Spring and libraries like Lombok are built. In this blog, I’m going to show you how to create your own annotation-based code generator in Java using the JavaPoet library.
Note: If you are not familiar with Java annotations and how they work, I recommend you check out my blog covering the topic. I have explained what annotations are and how to use them in detail.
Note: If you prefer a video format, you can watch it on youtube!
In this blog, I’m going to create a code generator for generating SQL database Data Access Object (DAO) classes, which will include Create, Read, Update, and Delete (CRUD) methods.
Quick Overview of the Approach
We’ll be creating an annotation processor class (MySqlProcessor) that will contain the logic to generate the code. This processor will rely on the use of the following custom annotations by the user in their class: MySqlGenerated, Persisted, and UniqueKey. The MySqlGenerated annotation is intended to be used on the DTO (or model class with getter and setter methods). The properties in the class will represent columns in the SQL table and will be annotated with Persisted or UniqueKey to indicate whether the field value must be stored in the database and whether the field is the primary key in the table, respectively. Below is a sample DTO class.
import com.gogettergeeks.annotation.FileDBGenerated;
import com.gogettergeeks.annotation.Persisted;
import com.gogettergeeks.annotation.UniqueKey;
@MySqlGenerated
public class Student {
@Persisted
private String name;
@Persisted
@UniqueKey
private int rollNumber;
private double percentage;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getRollNumber() {
return rollNumber;
}
public void setRollNumber(int rollNumber) {
this.rollNumber = rollNumber;
}
}
You can find the entire source code here. For the rest of the blog, we’ll focus on creating the processor class code using JavaPoet.
About Java Poet
JavaPoet is an open-source library that provides APIs to generate Java source code from annotations or metadata. For example, if you need to create a method, you’ll create a MethodSpec object, which follows the builder pattern. Here is a sample MethodSpec for generating the following method:
public void sayHello(String name) {
System.out.println("Hello, " + name);
}
MethodSpec sayHelloMethodSpec = MethodSpec.methodBuilder("sayHello")
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(String.class, "name")
.addStatement("$T.out.println($S + name)", System.class, "Hello, ")
.build();
Similar to MethodSpec, JavaPoet offers various classes for generating different elements of code, such as TypeSpec for creating classes, ParameterSpec for creating parameters, and so on. For more examples and detailed documentation, you can check out the README of the official repository.
MySqlProcessor
You can find the entire source code here. I’m going to explain the critical portions of the code below.
- Setup
If you want Java to run your processor class during compilation, you need to extend the javax.annotation.processing.AbstractProcessor class. Once you extend AbstractProcessor, you need to override the process method, where our entire code-generation logic will reside. You'll then get a list of all the classes annotated with the MySqlGenerated annotation and extract fields annotated with Persisted and UniqueKey. For this example, we'll also extract the class name provided by the user and the package name. This information will be used to determine the naming convention for the generated class and the location where the generated classes must be stored. For example, if the user names their class Student, then we'll generate two classes: StudentGeneratedDto and StudentDao. StudentGeneratedDto will be our internal representation of the DTO, used as the model, and will only include fields marked with the Persisted or UniqueKey annotations. The Dao class will contain all four methods. We'll be creating private methods to generate both of these classes.
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
boolean isClaimed = false;
Set<? extends Element> fileDbGeneratedAnnotatedClasses = roundEnv.getElementsAnnotatedWith(MySqlGenerated.class);
for (Element element : fileDbGeneratedAnnotatedClasses) {
if (element.getKind() == ElementKind.CLASS) {
TypeElement classElement = (TypeElement) element;
List<VariableElement> fields = new ArrayList<>();
List<VariableElement> uniqueKeyFields = new ArrayList<>();
for (Element enclosedElement : classElement.getEnclosedElements()) {
if (enclosedElement.getKind() == ElementKind.FIELD
&& enclosedElement.getAnnotation(UniqueKey.class) != null) {
VariableElement fieldElement = (VariableElement) enclosedElement;
uniqueKeyFields.add(fieldElement);
} else if (enclosedElement.getKind() == ElementKind.FIELD
&& enclosedElement.getAnnotation(Persisted.class) != null) {
VariableElement fieldElement = (VariableElement) enclosedElement;
fields.add(fieldElement);
}
}
if (!fields.isEmpty()) {
TypeElement enclosingClass = (TypeElement) fields.stream().findAny().get().getEnclosingElement();
this.packageName = processingEnv.getElementUtils().getPackageOf(enclosingClass).toString();
this.className = enclosingClass.getSimpleName().toString();
generateDto(uniqueKeyFields, fields);
generateDao(uniqueKeyFields, fields);
}
}
}
return isClaimed;
}
2. Generating the GeneratedDto Class
We’ll iterate through the list of fields annotated with Persisted and UniqueKey annotations and use JavaPoet to generate the setter and getter methods. We'll create a list of MethodSpec and FieldSpec objects, which will include fields and methods (setter and getter) for every field in the user-provided DTO. We will use the MethodSpec class for setter and getter methods, FieldSpec for declaring fields, and TypeSpec for the GeneratedDto class.
List<MethodSpec> methodSpecs = new ArrayList<>();
List<FieldSpec> fieldsSpec = new ArrayList<>();
for (VariableElement uniqueKey : uniqueKeyFields) {
String fieldName = uniqueKey.getSimpleName().toString();
FieldSpec fieldSpec = FieldSpec.builder(TypeName.get(uniqueKey.asType()), fieldName)
.addModifiers(Modifier.PRIVATE)
.build();
MethodSpec setterMethodSpec = MethodSpec.methodBuilder("set" + StringUtil.capitalizeFirstLetter(fieldName))
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(TypeName.get(uniqueKey.asType()), fieldName)
.addStatement("this." + fieldName + " = " + fieldName)
.build();
MethodSpec getterMethodSpec = MethodSpec.methodBuilder("get" + StringUtil.capitalizeFirstLetter(fieldName))
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.get(uniqueKey.asType()))
.addStatement("return this." + fieldName)
.build();
fieldsSpec.add(fieldSpec);
methodSpecs.add(setterMethodSpec);
methodSpecs.add(getterMethodSpec);
}
for (VariableElement field : fields) {
String fieldName = field.getSimpleName().toString();
FieldSpec fieldSpec = FieldSpec.builder(TypeName.get(field.asType()), fieldName)
.addModifiers(Modifier.PRIVATE)
.build();
MethodSpec setterMethodSpec = MethodSpec.methodBuilder("set" + StringUtil.capitalizeFirstLetter(fieldName))
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(TypeName.get(field.asType()), fieldName)
.addStatement("this." + fieldName + " = " + fieldName)
.build();
MethodSpec getterMethodSpec = MethodSpec.methodBuilder("get" + StringUtil.capitalizeFirstLetter(fieldName))
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.get(field.asType()))
.addStatement("return this." + fieldName)
.build();
fieldsSpec.add(fieldSpec);
methodSpecs.add(setterMethodSpec);
methodSpecs.add(getterMethodSpec);
}
TypeSpec generatedDtoSpec = TypeSpec.classBuilder(className + DTO_SUFFIX)
.addModifiers(Modifier.PUBLIC)
.addFields(fieldsSpec)
.addMethods(methodSpecs)
.build();
And lastly, we’ll use a combination of JavaPoet’s JavaFile class and the filer instance provided by AbstractProcessor to save the generated file to disk.
JavaFile javaFile = JavaFile.builder(packageName, generatedDtoSpec).build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
3. Generating the DAO Class
Note: I will cover the create and read methods in this blog, as update and delete methods are very similar to create.
I usually keep sample code ready and then make it generic as part of the code-generation process. Below is the sample code for the create method.
public void create(Students students) throws SQLException {
StudentsGeneratedDto generatedDto = convert(students);
String query = "INSERT INTO students(roll_number, name) VALUES (?, ?)";
try (Connection connection = DriverManager.getConnection(this.connectionUrl, username, password);
PreparedStatement preparedStatement = connection.prepareStatement(query)
) {
preparedStatement.setInt(1, generatedDto.getRollNumber());
preparedStatement.setString(2, generatedDto.getName());
preparedStatement.executeUpdate();
};
}
Here, the tricky part is constructing the query. To do this, we’ll iterate over all the fields, keeping track of the number of question marks needed (one per field). We will also construct the PreparedStatement setter methods, as we need to know the datatype of each field. Since we're already iterating through the fields, it makes sense to complete this in a single iteration.
Since we’re building multiple statements at once, we can use the CodeBlock class provided by JavaPoet. It acts like StringBuilder, allowing you to append strings without worrying about creating multiple objects internally, as it is mutable. Here is the final version of the create method:
private MethodSpec getCreateMethodSpec(List<VariableElement> uniqueFields, List<VariableElement> fields) {
CodeBlock.Builder methodBodyBuilder = CodeBlock.builder();
methodBodyBuilder.add(className + DTO_SUFFIX + " generatedDto = convert(" + className.toLowerCase() + ");");
methodBodyBuilder.add("String query = \"INSERT INTO " + className.toLowerCase() + "(");
StringBuilder questionMarks = new StringBuilder();
StringBuilder preparedStatements = new StringBuilder();
int queryParamIndex = 1;
for (int i=0; i < uniqueFields.size(); i++, queryParamIndex++) {
VariableElement uniqueField = uniqueFields.get(i);
String columnName = StringUtil.camelCaseToUnderscore(uniqueField.getSimpleName().toString());
methodBodyBuilder.add(columnName);
questionMarks.append("?");
String variableSimpleTypeName = uniqueField.asType().getKind().isPrimitive() ?
uniqueField.asType().toString() : getSimpleClassName(uniqueField);
preparedStatements.append("preparedStatement.set")
.append(StringUtil.capitalizeFirstLetter(variableSimpleTypeName))
.append("(").append(queryParamIndex).append(", generatedDto.get")
.append(StringUtil.capitalizeFirstLetter(uniqueField.getSimpleName().toString())).append("());");
if (fields.size() > 0) {
methodBodyBuilder.add(", ");
questionMarks.append(", ");
} else {
if (i != uniqueFields.size()-1) {
methodBodyBuilder.add(", ");
questionMarks.append(", ");
}
}
}
for (int i=0; i < fields.size(); i++, queryParamIndex++) {
VariableElement field = fields.get(i);
String columnName = StringUtil.camelCaseToUnderscore(field.getSimpleName().toString());
methodBodyBuilder.add(columnName);
questionMarks.append("?");
String variableSimpleTypeName = field.asType().getKind().isPrimitive() ?
field.asType().toString() : getSimpleClassName(field);
preparedStatements.append("preparedStatement.set")
.append(StringUtil.capitalizeFirstLetter(variableSimpleTypeName))
.append("(").append(queryParamIndex).append(", generatedDto.get")
.append(StringUtil.capitalizeFirstLetter(field.getSimpleName().toString())).append("());");
if (i != fields.size()-1) {
methodBodyBuilder.add(", ");
questionMarks.append(", ");
}
}
preparedStatements.append("preparedStatement.executeUpdate();");
methodBodyBuilder.add(") VALUES (" + questionMarks + ")\";");
methodBodyBuilder.add("try ($T connection = $T.getConnection(this.connectionUrl, username, password);",
Connection.class, DriverManager.class);
methodBodyBuilder.add("$T preparedStatement = connection.prepareStatement(query)) {", PreparedStatement.class);
methodBodyBuilder.add(preparedStatements.toString());
methodBodyBuilder.add("}");
return MethodSpec.methodBuilder("create")
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get(packageName, className), className.toLowerCase())
.addException(SQLException.class)
.addStatement(methodBodyBuilder.build())
.build();
}
Next, let’s talk about the read method. Here is a sample read method:
public List < Students > read() throws SQLException {
List < Students > dtoList = new ArrayList < Students > ();
String query = "SELECT * FROM students";
try (Connection connection = DriverManager.getConnection(this.connectionUrl, username, password);
Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery(query)
) {
while (resultSet.next()) {
Students dto = new Students();
dto.setRollNumber(resultSet.getInt("roll_number"));
dto.setName(resultSet.getString("name"));
dtoList.add(dto);
}
}
return dtoList;
}
In the read method, the tricky part is specifying the parameterized List and ArrayList classes. JavaPoet provides the ClassName class, which takes in the package name and class name to construct a ClassName object. Once you have the ClassName object, you can use the ParameterizedTypeName class, which takes in the type and the type of the parameter.
ClassName list = ClassName.get("java.util", "List");
ClassName arrayList = ClassName.get("java.util", "ArrayList");
ClassName userProvidedDto = ClassName.get(packageName, className);
CodeBlock.Builder methodBodyBuilder = CodeBlock.builder();
methodBodyBuilder.add("$T dtoList = new $T();", ParameterizedTypeName.get(list, userProvidedDto),
ParameterizedTypeName.get(arrayList, userProvidedDto));
The rest of the code is pretty straightforward.
Some additional features of Java Poet
There are some additional features of JavaPoet that I have used in creating the Processor class. Let’s discuss them briefly!
- Placeholders: Just as we use %s in String.format() to substitute placeholder values, JavaPoet provides three placeholders: $T, $S, and $L. $T is used for specifying types (classes). The advantage of $T is that JavaPoet will automatically add the import statement for the type in the generated code. $S is used for specifying strings; the $S placeholder is replaced with the provided value, enclosed in double quotes. For example, if the provided value is John, the generated code will replace it with "John", so you don't need to explicitly escape double quote characters. $L is used for specifying any literal and acts like $S, except double quotes are not added.
- Getting the Type of the Generated Class: In our example, we generated the GeneratedDto class and then used it as a type in the DAO at multiple places. To specify the type of generated classes, JavaPoet provides the ClassName class, which takes in the package name and class name to generate the Type object.
Conclusion
Personally, I enjoyed working with JavaPoet. It formalizes the code generation process by offering APIs. While it doesn’t necessarily reduce the lines of code needed to generate the source code, it makes the code more readable and handles much of the complexity around code generation, such as managing class imports.
If you liked the blog, please give it some claps. If you have feedback or questions, feel free to comment. You can find the complete source code here.
Annotation based Code Generator using Java Poet was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Aditya Solge

Aditya Solge | Sciencx (2024-08-23T11:34:20+00:00) Annotation based Code Generator using Java Poet. Retrieved from https://www.scien.cx/2024/08/23/annotation-based-code-generator-using-java-poet/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.