深入理解Java注解——JavaPoet使用

Posted yubo_725

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解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深入理解Java注解——JavaPoet使用

深入理解Java注解——注解基础

深入理解Java注解——注解基础

深入理解Java注解——注解基础

深入理解Java注解——编译时注解实战

深入理解Java注解——编译时注解实战