Java Code Generation Using JavaPoet

John AmiscarayJohn Amiscaray
6 min read

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!

10
Subscribe to my newsletter

Read articles from John Amiscaray directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

John Amiscaray
John Amiscaray