深入理解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源代码。
通过以上的例子,可以发现如下规律:
- JavaPoet中基本都使用建造者模式去做开发,生成类、方法、成员变量等各种对象时,都通过Builder去构造它们。
MethodSpec
代表一个方法,通过建造者模式,可以为方法添加修饰器(public, private等)、返回值、参数等。TypeSpec
代表一个类、接口或枚举,可以为这个类添加修饰器或方法。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使用