1. 概要

このチュートリアルでは、JavaPoetライブラリの基本的な機能について説明します。

JavaPoetSquareによって開発され、はJavaソースコードを生成するためのAPIを提供します。 プリミティブ型、参照型とそのバリアント(クラス、インターフェイス、列挙型、匿名内部クラスなど)、フィールド、メソッド、パラメーター、アノテーション、およびJavadocを生成できます。

JavaPoetは、依存クラスのインポートを自動的に管理します。 また、Builderパターンを使用して、Javaコードを生成するロジックを指定します。

2. Mavenの依存関係

JavaPoetを使用するには、最新の JARファイルを直接ダウンロードするか、 pom.xmlで次の依存関係を定義します:

<dependency>
    <groupId>com.squareup</groupId>
    <artifactId>javapoet</artifactId>
    <version>1.10.0</version>
</dependency>

3. メソッド仕様

まず、メソッドの仕様を見ていきましょう。 メソッドを生成するには、 MethodSpecクラスのmethodBuilder()メソッドを呼び出すだけです。 生成されたメソッド名は、 methodBuilder()メソッドのString引数として指定します。

addStatement()メソッドを使用して、セミコロンで終わる任意の単一の論理ステートメントを生成できます。 一方、制御フローでは、if-elseブロックやforループなど、中括弧で囲まれた1つの制御フローを定義できます。

簡単な例を次に示します。0から10までの数値の合計を計算するsumOfTen()メソッドを生成します。

MethodSpec sumOfTen = MethodSpec
  .methodBuilder("sumOfTen")
  .addStatement("int sum = 0")
  .beginControlFlow("for (int i = 0; i <= 10; i++)")
  .addStatement("sum += i")
  .endControlFlow()
  .build();

これにより、次の出力が生成されます。

void sumOfTen() {
    int sum = 0;
    for (int i = 0; i <= 10; i++) {
        sum += i;
    }
}

4. コードブロック

1つ以上の制御フローと論理ステートメントを1つのコードブロックにラップすることもできます

CodeBlock sumOfTenImpl = CodeBlock
  .builder()
  .addStatement("int sum = 0")
  .beginControlFlow("for (int i = 0; i <= 10; i++)")
  .addStatement("sum += i")
  .endControlFlow()
  .build();

生成するもの:

int sum = 0;
for (int i = 0; i <= 10; i++) {
    sum += i;
}

addCode()を呼び出し、 sumOfTenImpl オブジェクトを提供することで、MethodSpecの以前のロジックを簡略化できます。

MethodSpec sumOfTen = MethodSpec
  .methodBuilder("sumOfTen")
  .addCode(sumOfTenImpl)
  .build();

コードブロックは、タイプやJavadocなどの他の仕様にも適用できます。

5. フィールド仕様

次へ–フィールド仕様ロジックを調べてみましょう。

フィールドを生成するには、 FieldSpecクラスのbuilder()メソッドを使用します。

FieldSpec name = FieldSpec
  .builder(String.class, "name")
  .addModifiers(Modifier.PRIVATE)
  .build();

これにより、次のフィールドが生成されます。

private String name;

initializer()メソッドを呼び出すことにより、フィールドのデフォルト値を初期化することもできます。

FieldSpec defaultName = FieldSpec
  .builder(String.class, "DEFAULT_NAME")
  .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
  .initializer("\"Alice\"")
  .build();

生成するもの:

private static final String DEFAULT_NAME = "Alice";

6. パラメータ仕様

それでは、パラメータ指定ロジックについて見ていきましょう。

メソッドにパラメーターを追加する場合は、ビルダーの関数呼び出しのチェーン内で addParameter()を呼び出すことができます。

より複雑なパラメータータイプの場合は、ParameterSpecビルダーを使用できます。

ParameterSpec strings = ParameterSpec
  .builder(
    ParameterizedTypeName.get(ClassName.get(List.class), TypeName.get(String.class)), 
    "strings")
  .build();

publicstatic:などのメソッドの修飾子を追加することもできます

MethodSpec sumOfTen = MethodSpec
  .methodBuilder("sumOfTen")
  .addParameter(int.class, "number")
  .addParameter(strings)
  .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
  .addCode(sumOfTenImpl)
  .build();

生成されたJavaコードは次のようになります。

public static void sumOfTen(int number, List<String> strings) {
    int sum = 0;
    for (int i = 0; i <= 10; i++) {
        sum += i;
    }
}

7. タイプ仕様

メソッド、フィールド、およびパラメーターを生成する方法を検討した後、タイプの仕様を見てみましょう。

型を宣言するには、クラス、インターフェイス、および列挙型を構築できるTypeSpecを使用できます。

7.1. クラスの生成

クラスを生成するために、 TypeSpecクラスのclassBuilder()メソッドを使用できます。

たとえば、publicおよびfinalアクセス修飾子などの修飾子を指定することもできます。 クラス修飾子に加えて、すでに述べたFieldSpecおよびMethodSpecクラスを使用してフィールドとメソッドを指定することもできます。

addField()および addMethod()メソッドは、インターフェイスまたは匿名内部クラスを生成するときにも使用できることに注意してください。

次のクラスビルダーの例を見てみましょう。

TypeSpec person = TypeSpec
  .classBuilder("Person")
  .addModifiers(Modifier.PUBLIC)
  .addField(name)
  .addMethod(MethodSpec
    .methodBuilder("getName")
    .addModifiers(Modifier.PUBLIC)
    .returns(String.class)
    .addStatement("return this.name")
    .build())
  .addMethod(MethodSpec
    .methodBuilder("setName")
    .addParameter(String.class, "name")
    .addModifiers(Modifier.PUBLIC)
    .returns(String.class)
    .addStatement("this.name = name")
    .build())
  .addMethod(sumOfTen)
  .build();

生成されたコードは次のようになります。

public class Person {
    private String name;

    public String getName() {
        return this.name;
    }

    public String setName(String name) {
        this.name = name;
    }

    public static void sumOfTen(int number, List<String> strings) {
        int sum = 0;
        for (int i = 0; i <= 10; i++) {
            sum += i;
        }
    }
}

7.2. インターフェイスの生成

Javaインターフェイスを生成するには、 interfaceBuilder() の方法 TypeSpec。

addModifiers() DEFAULT 修飾子の値を指定して、デフォルトのメソッドを定義することもできます。

TypeSpec person = TypeSpec
  .interfaceBuilder("Person")
  .addModifiers(Modifier.PUBLIC)
  .addField(defaultName)
  .addMethod(MethodSpec
    .methodBuilder("getName")
    .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
    .build())
  .addMethod(MethodSpec
    .methodBuilder("getDefaultName")
    .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
    .addCode(CodeBlock
      .builder()
      .addStatement("return DEFAULT_NAME")
      .build())
    .build())
  .build();

次のJavaコードが生成されます。

public interface Person {
    private static final String DEFAULT_NAME = "Alice";

    void getName();

    default void getDefaultName() {
        return DEFAULT_NAME;
    }
}

7.3. 列挙型の生成

列挙型を生成するには、 TypeSpecenumBuilder()メソッドを使用できます。 列挙された各値を指定するには、 addEnumConstant()メソッドを呼び出します。

TypeSpec gender = TypeSpec
  .enumBuilder("Gender")
  .addModifiers(Modifier.PUBLIC)
  .addEnumConstant("MALE")
  .addEnumConstant("FEMALE")
  .addEnumConstant("UNSPECIFIED")
  .build();

前述のenumBuilder()ロジックの出力は次のとおりです。

public enum Gender {
    MALE,
    FEMALE,
    UNSPECIFIED
}

7.4. 匿名の内部クラスの生成

匿名の内部クラスを生成するには、 TypeSpecクラスのanonymousClassBuilder()メソッドを使用できます。 addSuperinterface()メソッドで親クラスを指定する必要があることに注意してください。 それ以外の場合は、デフォルトの親クラスであるObjectを使用します。

TypeSpec comparator = TypeSpec
  .anonymousClassBuilder("")
  .addSuperinterface(ParameterizedTypeName.get(Comparator.class, String.class))
  .addMethod(MethodSpec
    .methodBuilder("compare")
    .addModifiers(Modifier.PUBLIC)
    .addParameter(String.class, "a")
    .addParameter(String.class, "b")
    .returns(int.class)
    .addStatement("return a.length() - b.length()")
    .build())
  .build();

これにより、次のJavaコードが生成されます。

new Comparator<String>() {
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

8. 注釈仕様

生成されたコードに注釈を追加するには、 MethodSpecまたはFieldSpecビルダークラスでaddAnnotation()メソッドを呼び出すことができます。

MethodSpec sumOfTen = MethodSpec
  .methodBuilder("sumOfTen")
  .addAnnotation(Override.class)
  .addParameter(int.class, "number")
  .addParameter(strings)
  .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
  .addCode(sumOfTenImpl)
  .build();

生成するもの:

@Override
public static void sumOfTen(int number, List<String> strings) {
    int sum = 0;
    for (int i = 0; i <= 10; i++) {
        sum += i;
    }
}

メンバー値を指定する必要がある場合は、 AnnotationSpecクラスのaddMember()メソッドを呼び出すことができます。

AnnotationSpec toString = AnnotationSpec
  .builder(ToString.class)
  .addMember("exclude", "\"name\"")
  .build();

これにより、次の注釈が生成されます。

@ToString(
    exclude = "name"
)

9. Javadocの生成

Javadocは、 CodeBlock、を使用するか、値を直接指定することで生成できます。

MethodSpec sumOfTen = MethodSpec
  .methodBuilder("sumOfTen")
  .addJavadoc(CodeBlock
    .builder()
    .add("Sum of all integers from 0 to 10")
    .build())
  .addAnnotation(Override.class)
  .addParameter(int.class, "number")
  .addParameter(strings)
  .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
  .addCode(sumOfTenImpl)
  .build();

これにより、次のJavaコードが生成されます。

/**
 * Sum of all integers from 0 to 10
 */
@Override
public static void sumOfTen(int number, List<String> strings) {
    int sum = 0;
    for (int i = 0; i <= 10; i++) {
        sum += i;
    }
}

10. フォーマット

セクション5FieldSpec初期化子の例を再確認してみましょう。このイニシャライザには、「Alice」String値をエスケープするために使用されるエスケープ文字が含まれています。

initializer("\"Alice\"")

セクション8にも、注釈の除外メンバーを定義するときの同様の例があります。

addMember("exclude", "\"name\"")

JavaPoetコードが大きくなり、同様のStringエスケープまたはString連結ステートメントが多数あると扱いにくくなります。

JavaPoetの文字列フォーマット機能により、 beginControlFlow() addStatement()、または initializer()メソッドでのStringフォーマットが簡単になります。 構文は、JavaのString.format()機能に似ています。 リテラル、文字列、タイプ、および名前のフォーマットに役立ちます

10.1. リテラルフォーマット

JavaPoetは、出力で$Lをリテラル値に置き換えます。引数に任意のプリミティブ型とString値を指定できます。

private MethodSpec generateSumMethod(String name, int from, int to, String operator) {
    return MethodSpec
      .methodBuilder(name)
      .returns(int.class)
      .addStatement("int sum = 0")
      .beginControlFlow("for (int i = $L; i <= $L; i++)", from, to)
      .addStatement("sum = sum $L i", operator)
      .endControlFlow()
      .addStatement("return sum")
      .build();
}

次の値を指定してgenerateSumMethod()を呼び出す場合:

generateSumMethod("sumOfOneHundred", 0, 100, "+");

JavaPoetは次の出力を生成します。

int sumOfOneHundred() {
    int sum = 0;
    for (int i = 0; i <= 100; i++) {
        sum = sum + i;
    }
    return sum;
}

10.2. 文字列フォーマット

String フォーマットでは、引用符が付いた値が生成されます。これは、JavaのStringタイプのみを参照します。 JavaPoetは、出力で$Sを文字列値に置き換えます。

private static MethodSpec generateStringSupplier(String methodName, String fieldName) {
    return MethodSpec
      .methodBuilder(methodName)
      .returns(String.class)
      .addStatement("return $S", fieldName)
      .build();
}

generateGetter()メソッドを呼び出して、次の値を提供する場合:

generateStringSupplier("getDefaultName", "Bob");

次の生成されたJavaコードを取得します。

String getDefaultName() {
    return "Bob";
}

10.3. タイプフォーマット

JavaPoetは、$Tを生成されたJavaコードのタイプに置き換えます。 JavaPoetは、インポートステートメントのタイプを自動的に処理します。 代わりに型をリテラルとして指定した場合、JavaPoetはインポートを処理しません。

MethodSpec getCurrentDateMethod = MethodSpec
  .methodBuilder("getCurrentDate")
  .returns(Date.class)
  .addStatement("return new $T()", Date.class)
  .build();

JavaPoetは次の出力を生成します。

Date getCurrentDate() {
    return new Date();
}

10.4. 名前の書式設定

変数/パラメーター、フィールド、またはメソッドの名前を参照する必要がある場合は、JavaPoetのStringフォーマッターで$Nを使用できます。

以前のgetCurrentDateMethod()を新しい参照メソッドに追加できます。

MethodSpec dateToString = MethodSpec
  .methodBuilder("getCurrentDateAsString")
  .returns(String.class)
  .addStatement(
    "$T formatter = new $T($S)", 
    DateFormat.class, 
    SimpleDateFormat.class, 
    "MM/dd/yyyy HH:mm:ss")
  .addStatement("return formatter.format($N())", getCurrentDateMethod)
  .build();

生成するもの:

String getCurrentDateAsString() {
    DateFormat formatter = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
    return formatter.format(getCurrentDate());
}

11. ラムダ式の生成

すでに調べた機能を利用して、ラムダ式を生成できます。 たとえば、 nameフィールドまたは変数を複数回出力するコードブロック:

CodeBlock printNameMultipleTimes = CodeBlock
  .builder()
  .addStatement("$T<$T> names = new $T<>()", List.class, String.class, ArrayList.class)
  .addStatement("$T.range($L, $L).forEach(i -> names.add(name))", IntStream.class, 0, 10)
  .addStatement("names.forEach(System.out::println)")
  .build();

このロジックは、次の出力を生成します。

List<String> names = new ArrayList<>();
IntStream.range(0, 10).forEach(i -> names.add(name));
names.forEach(System.out::println);

12. JavaFileを使用した出力の生成

JavaFileクラスは、生成されたコードの出力を構成および生成するのに役立ちます。 Javaコードを生成するには、 JavaFileをビルドし、パッケージ名とTypeSpecオブジェクトのインスタンスを提供します。

12.1. コードのインデント

デフォルトでは、JavaPoetはインデントに2つのスペースを使用します。 一貫性を保つために、このチュートリアルのすべての例では、 indent()メソッドを介して構成された4つのスペースインデントを使用して示しました。

JavaFile javaFile = JavaFile
  .builder("com.baeldung.javapoet.person", person)
  .indent("    ")
  .build();

12.2. 静的インポート

静的インポートを追加する必要がある場合は、 addStaticImport()メソッドを呼び出すことにより、JavaFileでタイプと特定のメソッド名を定義できます。

JavaFile javaFile = JavaFile
  .builder("com.baeldung.javapoet.person", person)
  .indent("    ")
  .addStaticImport(Date.class, "UTC")
  .addStaticImport(ClassName.get("java.time", "ZonedDateTime"), "*")
  .build();

これにより、次の静的インポートステートメントが生成されます。

import static java.util.Date.UTC;
import static java.time.ZonedDateTime.*;

12.3. 出力

writeTo()メソッドは、標準出力ストリーム( System.out )やFileなどの複数のターゲットにコードを書き込む機能を提供します。

Javaコードを標準の出力ストリームに書き込むには、 writeTo()メソッドを呼び出し、引数としてSystem.outを指定します。

JavaFile javaFile = JavaFile
  .builder("com.baeldung.javapoet.person", person)
  .indent("    ")
  .addStaticImport(Date.class, "UTC")
  .addStaticImport(ClassName.get("java.time", "ZonedDateTime"), "*")
  .build();

javaFile.writeTo(System.out);

writeTo()メソッドは、java.nio.file.Pathおよびjava.io.Fileも受け入れます。 Javaソースコードファイルを宛先フォルダー/パスに生成するために、対応するPathまたはFileオブジェクトを提供できます。

Path path = Paths.get(destinationPath);
javaFile.writeTo(path);

JavaFile の詳細については、Javadocを参照してください。

13. 結論

この記事は、メソッド、フィールド、パラメーター、タイプ、アノテーション、Javadocの生成などのJavaPoet機能の概要です。

JavaPoetは、コード生成専用に設計されています。 Javaでメタプログラミングを行いたい場合、バージョン1.10.0以降のJavaPoetはコードのコンパイルと実行をサポートしていません。

いつものように、例とコードスニペットはGitHubから入手できます。