深入理解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();
}
以上代码会生成下面的源代码:
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);
更多推荐
所有评论(0)