深入理解Java注解(二)——JavaPoet使用

什么是JavaPoet

JavaPoet是使用Java编写的一个库,主要用于生成Java源代码,其GitHub地址为:https://github.com/square/javapoet

之所以本篇会记录JavaPoet,主要是因为很多开源库都使用到了Java编译时注解,而处理注解时基本都用到了JavaPoet去生成新的Java代码,要想了解编译时注解的流程,必须先了解前置知识JavaPoet。

JavaPoet的使用

从JavaPoet的GitHub主页可以看到这个库的代码并不多,所有的类都位于com.squareup.javapoet包下,关于JavaPoet的用法,在GitHub主页的README中已经有很详细的示例代码了,建议大家可以直接查看其英文文档,如果对看英文文档不熟的朋友可以直接看我这篇博客,本篇主要也是对该英文文档的一个翻译,加上自己的一些实践代码。

一个简单的例子

这是一个无聊的HelloWorld类:

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

使用JavaPoet可以这么生成它:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)
    .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);

运行上面的代码,你可以在控制台中看到输出的Java源代码。

通过以上的例子,可以发现如下规律:

  1. JavaPoet中基本都使用建造者模式去做开发,生成类、方法、成员变量等各种对象时,都通过Builder去构造它们。
  2. MethodSpec代表一个方法,通过建造者模式,可以为方法添加修饰器(public, private等)、返回值、参数等。
  3. TypeSpec代表一个类、接口或枚举,可以为这个类添加修饰器或方法。
  4. JavaFile代表一个Java源代码文件,通过建造者模式可以设置包名、文件中的类,并且可以将这个文件内容输出(输出到控制台,或者到某个文件中)。

代码 & 控制流

看下面这段代码:

void main() {
  int total = 0;
  for (int i = 0; i < 10; i++) {
    total += i;
  }
}

这段代码中只有一个main方法,在方法体中通过for循环累加了total参数的值,如果使用JavaPoet生成这段代码,可以采用如下方法:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addCode(""
        + "int total = 0;\n"
        + "for (int i = 0; i < 10; i++) {\n"
        + "  total += i;\n"
        + "}\n")
    .build();

MethodSpec中的Builder里有一个addCode方法,表示在这个方法中添加一些代码片段,注意,这里的addCode方法的参数为一个字符串,且字符串中的换行和分号等都不能省略,你必须像写真实代码一样来写这个参数字符串。如果生成代码都这么写的话,肯定很容易出错,所以JavaPoet库封装了一些控制流API方便我们生成复杂的代码,比如我们使用beginControlFlow addStatement endControFlow等API实现上面的循环代码可以这么做:

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

beginControlFlow表示开始一个控制流,endControlFlow表示结束控制流,addStatement表示添加某个代码语句,可以看到这种方式比使用addCode的方式简洁许多,而且不需要你处理代码中的花括号、换行、分号等信息,API内部会自动处理这些信息。
针对if/else这种控制流语句,同样可以使用beginControlFlow nextControlFlow endControlFlow 相关API,比如要生成下面的代码:

private static void test(int score) {
    if (score >= 90) {
        System.out.println("very good");
    } else if (score >= 75) {
        System.out.println("not bad");
    } else if (score >= 60) {
        System.out.println("just so so");
    } else {
        System.out.println("worse");
    }
}

使用JavaPoet可以这么写:

MethodSpec methodSpec = MethodSpec.methodBuilder("test")
    .addParameter(int.class, "score")
    .beginControlFlow("if (score >= 90)")
    .addStatement("$T.out.println($S)", System.class, "very good")
    .nextControlFlow("else if (score >= 75)")
    .addStatement("$T.out.println($S)", System.class, "not bad")
    .nextControlFlow("else if (score >= 60)")
    .addStatement("$T.out.println($S)", System.class, "just so so")
    .nextControlFlow("else")
    .addStatement("$T.out.println($S)", System.class, "worse")
    .endControlFlow()
    .build();

这里需要注意的是,一定要有endControlFlow这句,如果遗漏这句的话,代码依然能正常生成,但是源码中会少一个右花括号。

$L $S占位符

$L占位符可以和$S对比学习,$S表示的是一个字符串,而$L则表示字面上的数据,比如下面的代码:

private static void test11(int a, int b) {
    MethodSpec methodSpec = MethodSpec.methodBuilder("test")
            .addStatement("int sum = $L + $L", a, b) // (1)
            .returns(int.class)
            .addStatement("return sum")
            .build();
    TypeSpec typeSpec = TypeSpec.classBuilder("Test")
            .addModifiers(Modifier.PUBLIC)
            .addMethod(methodSpec)
            .build();
    try {
        JavaFile.builder("com.example", typeSpec).build().writeTo(System.out);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

通过test11(1, 2)调用上面的代码,可以看到控制台输出如下:

package com.example;

public class Test {
  int test() {
    int sum = 1 + 2;
    return sum;
  }
}

如果把上面代码中注释(1)那行中的$L改成$S,会发现控制台输出如下:

package com.example;

public class Test {
  int test() {
    int sum = "1" + "2";
    return sum;
  }
}

很明显,$S占位符会将传入的参数当作字符串处理,如果参数不是字符串,则会转成字符串,但是$L会直接使用参数的字面值。

$T占位符

$T可以表示类、接口或枚举类型,比如要生成下面的代码:

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

用JavaPoet可以这么写:

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

对于$T占位符,还可以使用ClassName这种方式,比如要生成下面这段代码:

List<Object> list = new ArrayList<>();
list.add(1);
list.add("hello");
list.add(new Person("zhangsan")); // Person是定义在com.example.entity包下的一个类
Iterator<Object> iterator = list.iterator();
while (iterator.hasNext()) {
    Object obj = iterator.next();
    System.out.println("obj = " + obj);
}

可以使用如下方式:

ClassName personCls = ClassName.get("com.example.entity", "Person");
ClassName listCls = ClassName.get("java.util", "List");
ClassName arrayListCls = ClassName.get("java.util", "ArrayList");
ClassName objCls = ClassName.get("java.lang", "Object");
MethodSpec methodSpec = MethodSpec.methodBuilder("test")
        .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
        .returns(void.class)
        .addStatement("$T<$T> list = new $T<>()", listCls, objCls, arrayListCls)
        .addStatement("list.add(1)")
        .addStatement("list.add($S)", "hello")
        .addStatement("list.add(new $T($S))", personCls, "zhangsan")
        .addStatement("$T<$T> iterator = list.iterator()", Iterator.class, Object.class)
        .beginControlFlow("while (iterator.hasNext())")
        .addStatement("$T obj = iterator.next()", objCls)
        .addStatement("$T.out.println($S + obj)", System.class, "obj = ")
        .endControlFlow()
        .build();
TypeSpec typeSpec = TypeSpec.classBuilder("Test")
        .addModifiers(Modifier.PUBLIC)
        .addMethod(methodSpec)
        .build();
try {
    JavaFile.builder("com.example", typeSpec).build().writeTo(System.out);
} catch (IOException e) {
    e.printStackTrace();
}

运行以上代码,控制台打印如下:
在这里插入图片描述

$N占位符

$N占位符主要用于引用另一个使用JavaPoet生成的方法或其他变量,比如我们希望生成如下代码:

package com.example;

public class Calculator {

    public static int plus(int a, int b) {
        return a + b;
    }

    public static int sub(int a, int b) {
        return a - b;
    }
    
    public static void test(int a, int b) {
        System.out.println("a + b = " + plus(a, b));
        System.out.println("a - b = " + sub(a, b));
    }

}

使用JavaPoet可以这么写:

MethodSpec plus = MethodSpec.methodBuilder("plus")
        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
        .returns(int.class)
        .addParameter(int.class, "a")
        .addParameter(int.class, "b")
        .addStatement("return a + b")
        .build();
MethodSpec sub = MethodSpec.methodBuilder("sub")
        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
        .returns(int.class)
        .addParameter(int.class, "a")
        .addParameter(int.class, "b")
        .addStatement("return a - b")
        .build();
MethodSpec methodSpec = MethodSpec.methodBuilder("test")
        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
        .addParameter(int.class, "a")
        .addParameter(int.class, "b")
        .addStatement("$T.out.println($S + $N(a, b))", System.class, "a + b = ", plus)
        .addStatement("$T.out.println($S + $N(a, b))", System.class, "a - b = ", sub)
        .build();
TypeSpec typeSpec = TypeSpec.classBuilder("Calculator")
        .addModifiers(Modifier.PUBLIC)
        .addMethod(methodSpec)
        .addMethod(plus)
        .addMethod(sub)
        .build();
try {
    JavaFile.builder("com.example", typeSpec).build().writeTo(System.out);
} catch (IOException e) {
    e.printStackTrace();
}

运行以上代码,控制台打印如下:
在这里插入图片描述
$N也可以引用一个字段,比如下面的代码:

FieldSpec fieldSpec = FieldSpec.builder(String.class, "name")
        .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
        .initializer("$S", "hello world")
        .build();
MethodSpec methodSpec = MethodSpec.methodBuilder("sayHello")
        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
        .returns(void.class)
        .addStatement("$T.out.println($S + $N)", System.class, "Hello, this is ", fieldSpec)
        .build();
TypeSpec typeSpec = TypeSpec.classBuilder("Calculator")
        .addModifiers(Modifier.PUBLIC)
        .addMethod(methodSpec)
        .addField(fieldSpec)
        .build();
try {
    JavaFile.builder("com.example", typeSpec).build().writeTo(System.out);
} catch (IOException e) {
    e.printStackTrace();
}

生成的代码如下:
在这里插入图片描述

代码片段格式化

相对参数

相对参数即使用占位符时,使用的参数根据传入的参数位置来,比如下面的代码:

CodeBlock.builder().add("My name is $S, I'm $L years old", "Tom", 25).build();
// 输出:My name is "Tom", I'm 25 years old

位置参数

位置参数可以让占位符根据参数的位置来选择,比如下面的代码:

CodeBlock.builder().add("My name is $2S, I'm $1L years old", 25, "Tom").build();
// 输出:My name is "Tom", I'm 25 years old

$2S表示使用参数列表中的第二个参数,$1L表示使用参数列表中的第一个参数。

命名参数

命名参数可以给占位置指定一个名字,传参数时需要使用Map,比如下面的代码:

Map<String, Object> params = new HashMap<>();
params.put("name", "Tom");
params.put("age", 25);
CodeBlock block = CodeBlock.builder().addNamed("My name is $name:S, I'm $age:L years old", params).build();
System.out.println(block);
// 输出:My name is "Tom", I'm 25 years old

这里需要注意,使用命名参数时,必须用addNamed方法。

抽象类和抽象方法

上面的各种示例代码中生成的方法都是有方法体的,如果你想生成没有方法体的方法,可以这么做:

MethodSpec methodSpec = MethodSpec.methodBuilder("process")
        .addModifiers(Modifier.ABSTRACT)
        .returns(void.class)
        .build();
TypeSpec typeSpec = TypeSpec.classBuilder("AbstractProcessor")
        .addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
        .addMethod(methodSpec)
        .build();
try {
    JavaFile.builder("com.example", typeSpec).build().writeTo(System.out);
} catch (IOException e) {
    e.printStackTrace();
}

以上代码会生成下面的源代码:

package com.example;

public abstract class AbstractProcessor {
  abstract void process();
}

构造方法

要想为一个类添加构造方法,可以这么做:

FieldSpec fieldSpec = FieldSpec.builder(String.class, "name")
        .addModifiers(Modifier.PRIVATE)
        .build();
MethodSpec methodSpec = MethodSpec.constructorBuilder()
        .addModifiers(Modifier.PUBLIC)
        .addParameter(String.class, "name")
        .addStatement("this.name = name")
        .build();
TypeSpec typeSpec = TypeSpec.classBuilder("Person")
        .addModifiers(Modifier.PUBLIC)
        .addField(fieldSpec)
        .addMethod(methodSpec)
        .build();
try {
    JavaFile.builder("com.example", typeSpec).build().writeTo(System.out);
} catch (IOException e) {
    e.printStackTrace();
}

上面的代码会生成如下源码:

package com.example;

import java.lang.String;

public class Person {
  private String name;

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

为方法添加参数

使用addParameter可以在构造方法时给方法添加参数,在前面的示例代码中已经用到,这里不做过多的记录。

成员变量

为某个类添加成员变量可以使用FieldSpec构造,在前面的示例代码中已有相关使用,这里不做过多记录。

接口和枚举类

Java中的接口里可以定义某个成员变量,或者声明某个方法但没有方法的具体实现,其中成员变量默认是public static final的,方法默认是public abstract的,使用下面的方法生成Java接口:

MethodSpec methodSpec = MethodSpec.methodBuilder("hello")
        .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
        .build();
FieldSpec fieldSpec = FieldSpec.builder(String.class, "str")
        .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
        .build();
TypeSpec typeSpec = TypeSpec.interfaceBuilder("HelloWorld")
        .addModifiers(Modifier.PUBLIC)
        .addMethod(methodSpec)
        .addField(fieldSpec)
        .build();
try {
    JavaFile.builder("com.example", typeSpec).build().writeTo(System.out);
} catch (IOException e) {
    e.printStackTrace();
}

以上代码生成的源码如下:

package com.example;

import java.lang.String;

public interface HelloWorld {
  String str;

  void hello();
}

要生成枚举类,可以使用如下代码:

TypeSpec typeSpec = TypeSpec.enumBuilder("Color")
        .addModifiers(Modifier.PUBLIC)
        .addEnumConstant("RED")
        .addEnumConstant("BLUE")
        .addEnumConstant("BLACK")
        .build();
try {
    JavaFile.builder("com.example", typeSpec).build().writeTo(System.out);
} catch (IOException e) {
    e.printStackTrace();
}

以上代码生成的源码如下:

package com.example;

public enum Color {
  RED,

  BLUE,

  BLACK
}

匿名内部类

使用下面的方法生成匿名内部类:

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

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
        .addMethod(MethodSpec.methodBuilder("sortByLength")
                .addParameter(ParameterizedTypeName.get(List.class, String.class), "strings")
                .addStatement("$T.sort($N, $L)", Collections.class, "strings", comparator)
                .build())
        .build();
try {
    JavaFile.builder("com.example", helloWorld).build().writeTo(System.out);
} catch (IOException e) {
    e.printStackTrace();
}

以上代码输出如下源码:

package com.example;

import java.lang.Override;
import java.lang.String;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class HelloWorld {
  void sortByLength(List<String> strings) {
    Collections.sort(strings, new Comparator<String>() {
      @Override
      public int compare(String a, String b) {
        return a.length() - b.length();
      }
    });
  }
}

注解

生成注解可以使用如下方式:

MethodSpec toString = MethodSpec.methodBuilder("toString")
    .addAnnotation(Override.class)
    .returns(String.class)
    .addModifiers(Modifier.PUBLIC)
    .addStatement("return $S", "Hoverboard")
    .build();

以上代码生成源码为:

  @Override
  public String toString() {
    return "Hoverboard";
  }

也可以为注解添加一些属性:

MethodSpec logRecord = MethodSpec.methodBuilder("recordEvent")
    .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
    .addAnnotation(AnnotationSpec.builder(Headers.class)
        .addMember("accept", "$S", "application/json; charset=utf-8")
        .addMember("userAgent", "$S", "Square Cash")
        .build())
    .addParameter(LogRecord.class, "logRecord")
    .returns(LogReceipt.class)
    .build();

以上代码生成源码为:

@Headers(
    accept = "application/json; charset=utf-8",
    userAgent = "Square Cash"
)
LogReceipt recordEvent(LogRecord logRecord);

生成文档

MethodSpec dismiss = MethodSpec.methodBuilder("dismiss")
    .addJavadoc("Hides {@code message} from the caller's history. Other\n"
        + "participants in the conversation will continue to see the\n"
        + "message in their own history unless they also delete it.\n")
    .addJavadoc("\n")
    .addJavadoc("<p>Use {@link #delete($T)} to delete the entire\n"
        + "conversation for all participants.\n", Conversation.class)
    .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
    .addParameter(Message.class, "message")
    .build();

以上代码输出源码:

/**
   * Hides {@code message} from the caller's history. Other
   * participants in the conversation will continue to see the
   * message in their own history unless they also delete it.
   *
   * <p>Use {@link #delete(Conversation)} to delete the entire
   * conversation for all participants.
   */
  void dismiss(Message message);
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yubo_725

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值