Java Code Generation Using JavaPoet


Hi everyone. Welcome to a new blog post! Today, I’m excited to share a cool Java library I learned about called JavaPoet. Let’s dive straight into it by briefly discussing how it works and then showing it in action.
What Is JavaPoet?
JavaPoet is a library that makes programmatically generating Java source files easy and has been incredibly nice to use in a project I’m currently working on. Using mostly builders to form the different language constructs, you can build complex Java code using semantically elegant statements and fairly limited string formatting. Because of this, a lot of the code should be straightforward once I start giving you examples of it in practice!
NOTE: the original JavaPoet library by Square Inc. is now deprecated so it won’t be actively maintained and updated with the latest Java features. However, this library is still very usable and there is a fork being maintained by Palantir which contains some of the more recent Java features like records.
What We’ll Do With It
To give you a good feel for the library, we’ll generate a class that’s simple yet contains a variety of Java constructs for us to build. We’ll be writing code to replicate this class below:
package io.john.amiscaray;
import java.util.ArrayList;
import java.util.List;
public class ShoppingList {
private final List<String> items = new ArrayList<>();
private int capacity;
public ShoppingList(int capacity) {
this.capacity = capacity;
}
public List<String> getItems() {
return items;
}
public void setCapacity(int capacity) {
this.capacity = capacity;
}
public int getCapacity() {
return capacity;
}
public void addItem(String item) {
if (items.size() >= capacity) {
return;
}
items.add(item);
}
public void printItems() {
for (var item : items) {
System.out.println(item);
}
}
}
With different methods and block scopes, this should have different things for us to try out, yet be pretty straightforward for you to pick up.
Creating the Class
First, let me show you how we can create the class declaration without any fields or methods and print out the output. As I alluded to earlier, JavaPoet provides us with a lot of builders for generating Java code constructs, including classes:
package io.john.amiscaray;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.JavaFile;
import javax.lang.model.element.Modifier;
public class Main {
public static void main(String[] args) {
var shoppingListClass = TypeSpec.classBuilder("ShoppingList")
.addModifiers(Modifier.PUBLIC)
.build();
System.out.println(JavaFile.builder("io.john.amiscaray", shoppingListClass).build());
}
}
This code would have the following output:
package io.john.amiscaray;
public class ShoppingList {
}
Adding Fields
Now, we can add our fields to this class using the additional builder methods of the TypeSpec.Builder
class:
package io.john.amiscaray;
import com.squareup.javapoet.*;
import javax.lang.model.element.Modifier;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
var shoppingListClass = TypeSpec.classBuilder("ShoppingList")
.addField(
FieldSpec.builder(ParameterizedTypeName.get(List.class, String.class), "items")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.initializer("new $T()", ArrayList.class)
.build()
)
.addField(
FieldSpec.builder(ClassName.INT, "capacity")
.addModifiers(Modifier.PRIVATE)
.build()
)
.addModifiers(Modifier.PUBLIC)
.build();
System.out.println(JavaFile.builder("io.john.amiscaray", shoppingListClass).build());
}
}
Looking at the first TypeSpec.Builder#addField
call, we create a new field using the FieldSpec.Builder
class returned from the FieldSpec.Builder#builder
method call. This method takes the type of the field and its name. In this case, we are specifying a List<String>
field named items
. Then we add the private and final keywords to the field and initialize it as a new ArrayList<>()
. The FieldSpec.Builder#initializer
method takes a format string for the initial value of the field along with format arguments. In this case, the $T
token means a type name format argument, and the ArrayList.class
statement is the value we are using for it. At this point, the output looks like this:
package io.john.amiscaray;
import java.lang.String;
import java.util.ArrayList;
import java.util.List;
public class ShoppingList {
private final List<String> items = new ArrayList();
private int capacity;
}
Adding a Constructor, Getters, And Setters
Now, let’s add a constructor:
package io.john.amiscaray;
import com.squareup.javapoet.*;
import javax.lang.model.element.Modifier;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
var shoppingListClass = TypeSpec.classBuilder("ShoppingList")
.addField(
FieldSpec.builder(ParameterizedTypeName.get(List.class, String.class), "items")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.initializer("new $T()", ArrayList.class)
.build()
)
.addField(
FieldSpec.builder(ClassName.INT, "capacity")
.addModifiers(Modifier.PRIVATE)
.build()
)
.addMethod(MethodSpec
.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.INT, "capacity")
.addStatement("this.capacity = capacity")
.build()
)
.addModifiers(Modifier.PUBLIC)
.build();
System.out.println(JavaFile.builder("io.john.amiscaray", shoppingListClass).build());
}
}
Here, we call TypeSpec.Builder#addMethod
with a MethodSpec
created from a builder. You’ll notice that with the MethodSpec.Builder
we call MethodSpec.Builder#addStatement
to add code within the constructor body.
Similarly, we can make additional TypeSpec.Builder#addMethod
calls to generate our getters and setters:
package io.john.amiscaray;
import com.squareup.javapoet.*;
import javax.lang.model.element.Modifier;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
var shoppingListClass = TypeSpec.classBuilder("ShoppingList")
.addField(
FieldSpec.builder(ParameterizedTypeName.get(List.class, String.class), "items")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.initializer("new $T()", ArrayList.class)
.build()
)
.addField(
FieldSpec.builder(ClassName.INT, "capacity")
.addModifiers(Modifier.PRIVATE)
.build()
)
.addMethod(MethodSpec
.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.INT, "capacity")
.addStatement("this.capacity = capacity")
.build()
)
.addMethod(MethodSpec
.methodBuilder("getItems")
.addModifiers(Modifier.PUBLIC)
.returns(ParameterizedTypeName.get(List.class, String.class))
.addStatement("return items")
.build()
)
.addMethod(MethodSpec.methodBuilder("getCapacity")
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.INT)
.addStatement("return capacity")
.build())
.addMethod(MethodSpec.methodBuilder("setCapacity")
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeName.INT, "capacity")
.addStatement("this.capacity = capacity")
.build())
.addModifiers(Modifier.PUBLIC)
.build();
System.out.println(JavaFile.builder("io.john.amiscaray", shoppingListClass).build());
}
}
Now the code should output the following:
package io.john.amiscaray;
import java.lang.String;
import java.util.ArrayList;
import java.util.List;
public class ShoppingList {
private final List<String> items = new ArrayList();
private int capacity;
public ShoppingList(int capacity) {
this.capacity = capacity;
}
public List<String> getItems() {
return items;
}
public int getCapacity() {
return capacity;
}
public void setCapacity(int capacity) {
this.capacity = capacity;
}
}
Implementing Our Last Methods
Now, all we have to implement are the ShoppingList#addItem
and ShoppingList#printItems
methods. The following is the final code after implementing them:
package io.john.amiscaray;
import com.squareup.javapoet.*;
import javax.lang.model.element.Modifier;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
var shoppingListClass = TypeSpec.classBuilder("ShoppingList")
.addField(
FieldSpec.builder(ParameterizedTypeName.get(List.class, String.class), "items")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.initializer("new $T()", ArrayList.class)
.build()
)
.addField(
FieldSpec.builder(ClassName.INT, "capacity")
.addModifiers(Modifier.PRIVATE)
.build()
)
.addMethod(MethodSpec
.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.INT, "capacity")
.addStatement("this.capacity = capacity")
.build()
)
.addMethod(MethodSpec
.methodBuilder("getItems")
.addModifiers(Modifier.PUBLIC)
.returns(ParameterizedTypeName.get(List.class, String.class))
.addStatement("return items")
.build()
)
.addMethod(MethodSpec.methodBuilder("getCapacity")
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.INT)
.addStatement("return capacity")
.build())
.addMethod(MethodSpec.methodBuilder("setCapacity")
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeName.INT, "capacity")
.addStatement("this.capacity = capacity")
.build())
.addMethod(MethodSpec.methodBuilder("addItem")
.addModifiers(Modifier.PUBLIC)
.addParameter(String.class, "item")
.beginControlFlow("if (items.size() >= capacity)")
.addStatement("return")
.endControlFlow()
.addStatement("items.add(item)")
.build())
.addMethod(MethodSpec.methodBuilder("printItems")
.addModifiers(Modifier.PUBLIC)
.beginControlFlow("for (var item : items)")
.addStatement("System.out.println(item)")
.endControlFlow()
.build())
.addModifiers(Modifier.PUBLIC)
.build();
System.out.println(JavaFile.builder("io.john.amiscaray", shoppingListClass).build());
}
}
You’ll notice that we can create control flows (i.e., if
or for
statements) using the MethodSpec.Builder#beginControlFlow
method. Any statements after this method call get added within the new block scope until we call MethodSpec.Builder#endControlFlow
. With that, the final code should look like so:
package io.john.amiscaray;
import java.lang.String;
import java.util.ArrayList;
import java.util.List;
public class ShoppingList {
private final List<String> items = new ArrayList();
private int capacity;
public ShoppingList(int capacity) {
this.capacity = capacity;
}
public List<String> getItems() {
return items;
}
public int getCapacity() {
return capacity;
}
public void setCapacity(int capacity) {
this.capacity = capacity;
}
public void addItem(String item) {
if (items.size() >= capacity) {
return;
}
items.add(item);
}
public void printItems() {
for (var item : items) {
System.out.println(item);
}
}
}
Code Indentation
Lastly, you probably realized that the code we’ve been outputting isn’t well indented. We can quickly fix this by altering the line where we output the file:
System.out.println(JavaFile.builder("io.john.amiscaray", shoppingListClass).indent(" ").build());
Conclusion
With that, you should have a very good idea of how you can use the JavaPoet library to write beautiful code that generates Java files. Try it out yourself and build something awesome with it!
Subscribe to my newsletter
Read articles from John Amiscaray directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
