JavaPoet简介

2025/03/24

1. 概述

在本教程中,我们将探索JavaPoet库的基本功能。

JavaPoet由Square开发,提供生成Java源代码的API,可以生成原始类型、引用类型及其变体(如类、接口、枚举类型、匿名内部类)、字段、方法、参数、注解以及Javadocs。

JavaPoet自动管理依赖类的导入,它还使用构建器模式来指定生成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循环。

这是一个简单的例子-生成sumOfTen()方法,计算0到10之间的数字之和:

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. 代码块

我们还可以将一个或多个控制流和逻辑语句包装到一个代码块中

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();

代码块也适用于其他规范,例如类型和Javadocs。

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();

我们还可以添加方法的修饰符,例如public和/或static:

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接口,我们使用TypeSpec的interfaceBuilder()方法。

我们还可以通过在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 生成枚举

要生成枚举类型,我们可以使用TypeSpec的enumBuilder()方法。要指定每个枚举值,我们可以调用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. 生成Javadocs

可以使用CodeBlock生成Javadoc,或者直接指定值:

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. 格式化

让我们重新检查第5节中FieldSpec初始化程序的示例,其中包含用于转义“Alice”字符串值的转义字符:

initializer("\"Alice\"")

第8节中还有一个类似的例子,当我们定义注解的排除成员时:

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

当我们的JavaPoet代码增长并且具有大量类似的字符串转义或字符串拼接语句时,它就会变得难以处理。

JavaPoet中的字符串格式化功能使beginControlFlow()、addStatement()或initializer()方法中的字符串格式化更加容易。语法类似于Java中的String.format()功能,它可以帮助格式化文字、字符串、类型和名称

10.1 文字格式化

JavaPoet在输出中用文字值替换$L,我们可以在参数中指定任何原始类型和字符串值:

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 字符串格式化

字符串格式化会生成一个带引号的值,该值专指Java中的String类型。JavaPoet在输出中将$S替换为String值

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在生成的Java代码中用类型替换$T,JavaPoet会自动处理import语句中的类型。如果我们将类型作为文字提供,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的字符串格式化程序中使用$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. 生成Lambda表达式

我们可以利用已经探索过的功能来生成Lambda表达式。例如,多次打印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使用两个空格进行缩进,为了保持一致性,本教程中的所有示例都使用4个空格缩进,可通过indent()方法配置:

JavaFile javaFile = JavaFile
        .builder("cn.tuyucheng.taketoday.javapoet.person", person)
        .indent("    ")
        .build();

12.2 静态导入

如果我们需要添加静态导入,我们可以通过调用addStaticImport()方法在JavaFile中定义类型和具体方法名称:

JavaFile javaFile = JavaFile
        .builder("cn.tuyucheng.taketoday.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)和文件。

要将Java代码写入标准输出流,我们只需调用writeTo()方法,并提供System.out作为参数:

JavaFile javaFile = JavaFile
    .builder("cn.tuyucheng.taketoday.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,我们可以提供相应的Path或File对象,以便将Java源代码文件生成到目标文件夹/路径中:

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

有关JavaFile的更多详细信息,请参阅Javadoc

13. 总结

本文介绍了JavaPoet的功能,例如生成方法、字段、参数、类型、注解和Javadocs。

JavaPoet仅用于代码生成,如果我们想使用Java进行元编程,JavaPoet自1.10.0版本起不支持代码编译和运行。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章