(译)JavaPoet 官方教程
前言
JavaPoet
是一个用于生成 .Java
源文件的 Java API 。在执行注解处理或与元数据文件(例如,数据库模式、协议格式)交互等操作时,源文件生成可能很有用。通过生成代码,你可以消除样板代码,同时为元数据保留一个真实来源。
- Dependencies -
implementation 'com.squareup:javapoet:latest_release'
- Website - JavaPoet: A Java API for generating .java source files.
例子
这是一个(无聊的) 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);
为了声明 main 方法,我们创建了一个 MethodSpec
“main” ,它配置了修饰符、返回类型、参数和代码语句。我们将 main 方法添加到 HelloWorld
类中,然后将其添加到 HelloWorld.java
文件中。
在本例中,我们将文件写入 System.out
,但我们也可以将其作为字符串(JavaFile.toString()
)获取,或者将其写入文件系统(JavaFile.writeTo()
)。
代码与控制流程
JavaPoet 的大多数 API 都使用普通、不可变的 Java 对象,也提供了 Builder 模式、链式方法和可变参数来使 API 更加友好。JavaPoet 提供了类和接口(Typespec
)、字段(FieldSpec
)、方法和构造函数(MethodSpec
)、参数(ParameterSpec
)和注解(AnnotationSpec
)等模型。
但是方法和构造函数的主体没有建模。没有表达式类,没有语句类,也没有语法树节点。相反,JavaPoet 对代码块使用字符串:
MethodSpec main = MethodSpec.methodBuilder("main")
.addCode(""
+ "int total = 0;\n"
+ "for (int i = 0; i < 10; i++) {\n"
+ " total += i;\n"
+ "}\n")
.build();
以上将生成如下代码:
void main() {
int total = 0;
for (int i = 0; i < 10; i++) {
total += i;
}
}
手动写的分号、换行和缩进都很繁琐,因此 JavaPoet 提供了 API 来简化这些操作。addStatement()
负责分号和换行,而 beginControlFlow()
+ endControlFlow()
用于大括号、换行和缩进:
MethodSpec main = MethodSpec.methodBuilder("main")
.addStatement("int total = 0")
.beginControlFlow("for (int i = 0; i < 10; i++)")
.addStatement("total += i")
.endControlFlow()
.build();
这个例子很蹩脚,因为生成的代码是常量!假设不只是将 0 自增到 10,而是要使操作和范围可配置,下面是一个生成方法的方式:
private MethodSpec computeRange(String name, int from, int to, String op) {
return MethodSpec.methodBuilder(name)
.returns(int.class)
.addStatement("int result = 1")
.beginControlFlow("for (int i = " + from + "; i < " + to + "; i++)")
.addStatement("result = result " + op + " i")
.endControlFlow()
.addStatement("return result")
.build();
}
下面是我们调用 computeRange("multiply10to20", 10,20, "*")
时得到的结果:
int multiply10to20() {
int result = 1;
for (int i = 10; i < 20; i++) {
result = result * i;
}
return result;
}
方法生成方法!由于 JavaPoet 生成的是源代码而不是字节码,所以您可以阅读检查它以确保它是正确的。
一些控制流语句,例如 if/else ,可以具有无限的控制流可能性。你可以使用 nextControlFlow()
来处理这些选项:
MethodSpec main = MethodSpec.methodBuilder("main")
.addStatement("long now = $T.currentTimeMillis()", System.class)
.beginControlFlow("if ($T.currentTimeMillis() < now)", System.class)
.addStatement("$T.out.println($S)", System.class, "Time travelling, woo hoo!")
.nextControlFlow("else if ($T.currentTimeMillis() == now)", System.class)
.addStatement("$T.out.println($S)", System.class, "Time stood still!")
.nextControlFlow("else")
.addStatement("$T.out.println($S)", System.class, "Ok, time still moving forward")
.endControlFlow()
.build();
以上代码将生成:
void main() {
long now = System.currentTimeMillis();
if (System.currentTimeMillis() < now) {
System.out.println("Time travelling, woo hoo!");
} else if (System.currentTimeMillis() == now) {
System.out.println("Time stood still!");
} else {
System.out.println("Ok, time still moving forward");
}
}
使用 try/catch 捕获异常也是 nextControlFlow()
的一个用例:
MethodSpec main = MethodSpec.methodBuilder("main")
.beginControlFlow("try")
.addStatement("throw new Exception($S)", "Failed")
.nextControlFlow("catch ($T e)", Exception.class)
.addStatement("throw new $T(e)", RuntimeException.class)
.endControlFlow()
.build();
将生成:
void main() {
try {
throw new Exception("Failed");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
$L -> 输出字面量
调用 beginControlFlow()
和 addStatement
中的字符串连接会分散注意力,太多的运算符,为了解决这个问题,JavaPoet 提供了一种与 String.format()
类似但不兼容的语法。它接收 $L
并在输出中发出一个 字面量 。这就像 Formatter
的 %s
:
private MethodSpec computeRange(String name, int from, int to, String op) {
return MethodSpec.methodBuilder(name)
.returns(int.class)
.addStatement("int result = 0")
.beginControlFlow("for (int i = $L; i < $L; i++)", from, to)
.addStatement("result = result $L i", op)
.endControlFlow()
.addStatement("return result")
.build();
}
字面量 直接发送到输出代码,没有转义。文字的参数可以是字符串、基本数据类型和下面将描述的一些 JavaPoet 类型。
$S -> 输出字符串
在发出包含字符串文本的代码时,我们可以使用 $S
来发出 字符串 ,并使用引号和转义。下面代码输出 3 个方法,每个方法返回自己的名字:
public static void main(String[] args) throws Exception {
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(whatsMyName("slimShady"))
.addMethod(whatsMyName("eminem"))
.addMethod(whatsMyName("marshallMathers"))
.build();
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
javaFile.writeTo(System.out);
}
private static MethodSpec whatsMyName(String name) {
return MethodSpec.methodBuilder(name)
.returns(String.class)
.addStatement("return $S", name)
.build();
}
在这种情况下,使用 $S
给我们打上引号:
public final class HelloWorld {
String slimShady() {
return "slimShady";
}
String eminem() {
return "eminem";
}
String marshallMathers() {
return "marshallMathers";
}
}
$T -> 输出类型
我们 Java 程序员喜欢我们的类型:它们使我们的代码更容易理解。JavaPoet 也加入了。它对类型有丰富的内置支持,包括自动生成 import
语句。只需使用 $T
来引用 类型:
MethodSpec today = MethodSpec.methodBuilder("today")
.returns(Date.class)
.addStatement("return new $T()", Date.class)
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(today)
.build();
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
javaFile.writeTo(System.out);
将生成以下 .java
文件,并完成必要的 import
:
package com.example.helloworld;
import java.util.Date;
public final class HelloWorld {
Date today() {
return new Date();
}
}
我们传递 Date.class
来引用一个恰好在生成代码时可用的类。其实并不需要这样,下面是一个类似的例子,但是这个例子引用了一个不存在的类:
ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
MethodSpec today = MethodSpec.methodBuilder("tomorrow")
.returns(hoverboard)
.addStatement("return new $T()", hoverboard)
.build();
这个还不存在的类也被 import
:
package com.example.helloworld;
import com.mattel.Hoverboard;
public final class HelloWorld {
Hoverboard tomorrow() {
return new Hoverboard();
}
}
ClassName
类型非常重要,在使用 JavaPoet 时经常需要它。它可以识别任何已声明的类。声明类型只是 Java 丰富类型系统的开始:我们还有数组、参数化类型、通配符类型和类型变量。JavaPoet 有用于构建每个类的类:
ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
ClassName list = ClassName.get("java.util", "List");
ClassName arrayList = ClassName.get("java.util", "ArrayList");
TypeName listOfHoverboards = ParameterizedTypeName.get(list, hoverboard);
MethodSpec beyond = MethodSpec.methodBuilder("beyond")
.returns(listOfHoverboards)
.addStatement("$T result = new $T<>()", listOfHoverboards, arrayList)
.addStatement("result.add(new $T())", hoverboard)
.addStatement("result.add(new $T())", hoverboard)
.addStatement("result.add(new $T())", hoverboard)
.addStatement("return result")
.build();
JavaPoet 将分解每个类型,并在可能的地方 import
其组件。
package com.example.helloworld;
import com.mattel.Hoverboard;
import java.util.ArrayList;
import java.util.List;
public final class HelloWorld {
List<Hoverboard> beyond() {
List<Hoverboard> result = new ArrayList<>();
result.add(new Hoverboard());
result.add(new Hoverboard());
result.add(new Hoverboard());
return result;
}
}
$N -> 以名称引用生成的另一个声明
生成的代码通常是自引用的,使用 $N
以其名称引用另一个生成的声明。下面是一个方法中调用另一个方法:
public String byteToHex(int b) {
char[] result = new char[2];
result[0] = hexDigit((b >>> 4) & 0xf);
result[1] = hexDigit(b & 0xf);
return new String(result);
}
public char hexDigit(int i) {
return (char) (i < 10 ? i + '0' : i - 10 + 'a');
}
在生成上面的代码时,我们使用 $N
将 hexDigit()
方法作为参数传递给 byteToHex()
方法:
MethodSpec hexDigit = MethodSpec.methodBuilder("hexDigit")
.addParameter(int.class, "i")
.returns(char.class)
.addStatement("return (char) (i < 10 ? i + '0' : i - 10 + 'a')")
.build();
MethodSpec byteToHex = MethodSpec.methodBuilder("byteToHex")
.addParameter(int.class, "b")
.returns(String.class)
.addStatement("char[] result = new char[2]")
.addStatement("result[0] = $N((b >>> 4) & 0xf)", hexDigit)
.addStatement("result[1] = $N(b & 0xf)", hexDigit)
.addStatement("return new String(result)")
.build();
静态导入
JavaPoet 支持静态导入。它通过显式地收集类型成员名来实现这一点。让我们使用静态导入来增强前面的例子:
...
ClassName namedBoards = ClassName.get("com.mattel", "Hoverboard", "Boards");
MethodSpec beyond = MethodSpec.methodBuilder("beyond")
.returns(listOfHoverboards)
.addStatement("$T result = new $T<>()", listOfHoverboards, arrayList)
.addStatement("result.add($T.createNimbus(2000))", hoverboard)
.addStatement("result.add($T.createNimbus(\"2001\"))", hoverboard)
.addStatement("result.add($T.createNimbus($T.THUNDERBOLT))", hoverboard, namedBoards)
.addStatement("$T.sort(result)", Collections.class)
.addStatement("return result.isEmpty() ? $T.emptyList() : result", Collections.class)
.build();
TypeSpec hello = TypeSpec.classBuilder("HelloWorld")
.addMethod(beyond)
.build();
JavaFile.builder("com.example.helloworld", hello)
.addStaticImport(hoverboard, "createNimbus")
.addStaticImport(namedBoards, "*")
.addStaticImport(Collections.class, "*")
.build();
JavaPoet 将首先按照配置将您的静态导入块添加到文件中,匹配并处理所有调用,并根据需要导入所有其他类型。
package com.example.helloworld;
import static com.mattel.Hoverboard.Boards.*;
import static com.mattel.Hoverboard.createNimbus;
import static java.util.Collections.*;
import com.mattel.Hoverboard;
import java.util.ArrayList;
import java.util.List;
class HelloWorld {
List<Hoverboard> beyond() {
List<Hoverboard> result = new ArrayList<>();
result.add(createNimbus(2000));
result.add(createNimbus("2001"));
result.add(createNimbus(THUNDERBOLT));
sort(result);
return result.isEmpty() ? emptyList() : result;
}
}
代码块格式字符串
代码块可以通过几种方式指定占位符的值,代码块上的每个操作只能使用一种样式。
相对参数
将格式字符串中每个占位符的参数值传递给 CodeBlock.add()
。在每个例子中,我们生成代码说“我吃了3个玉米卷”
CodeBlock.builder().add("I ate $L $L", 3, "tacos")
位置参数
在格式字符串的占位符前放置一个整数索引(基于1),以指定使用哪个参数。
CodeBlock.builder().add("I ate $2L $1L", "tacos", 3)
命名参数
使用 $argumentName:X
语法,其中 X
是格式字符,使用格式字符串中包含所有参数键的映射调用 CodeBlock.addNamed()
。参数名使用 a-z
、A-Z
、0-9
和 _
中的字符,并且必须以小写字符开头。
Map<String, Object> map = new LinkedHashMap<>();
map.put("food", "tacos");
map.put("count", 3);
CodeBlock.builder().addNamed("I ate $count:L $food:L", map)
方法
上面所有的方法都有一个代码体。使用 Modifiers.ABSTRACT
得到一个没有任何主体的方法。只有当所包含的类是抽象类或接口类时,这才是合法的。
MethodSpec flux = MethodSpec.methodBuilder("flux")
.addModifiers(Modifier.ABSTRACT, Modifier.PROTECTED)
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.addMethod(flux)
.build();
将生成如下代码:
public abstract class HelloWorld {
protected abstract void flux();
}
其他修饰词在允许的地方起作用。注意,在指定修饰符时,JavaPoet 使用 javax.lang.model.element.Modifier
,一个在 Android 上不可用的类。此限制仅适用于代码生成代码,输出代码可以在任何地方运行:JVM 、Android 和 GWT。
方法还有参数、异常、变量变量、Javadoc、注解、类型变量和返回类型。所有这些都配置了 MethodSpec.Builder
。
构造函数
MethodSpec
是一个有点用词不当的词,它也可以用于构造函数:
MethodSpec flux = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(String.class, "greeting")
.addStatement("this.$N = $N", "greeting", "greeting")
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC)
.addField(String.class, "greeting", Modifier.PRIVATE, Modifier.FINAL)
.addMethod(flux)
.build();
将生成:
public class HelloWorld {
private final String greeting;
public HelloWorld(String greeting) {
this.greeting = greeting;
}
}
在大多数情况下,构造函数就像方法一样工作。在发出代码时,JavaPoet 会把构造函数放在输出文件中的方法之前。
参数
使用 ParameterSpec.builder()
或 MethodSpec
的 addParameter()
在方法和构造函数上声明参数:
ParameterSpec android = ParameterSpec.builder(String.class, "android")
.addModifiers(Modifier.FINAL)
.build();
MethodSpec welcomeOverlords = MethodSpec.methodBuilder("welcomeOverlords")
.addParameter(android)
.addParameter(String.class, "robot", Modifier.FINAL)
.build();
虽然上面生成 android
和 robot
参数的代码不同,但是输出是一样的:
void welcomeOverlords(final String android, final String robot) {
}
字段
与参数一样,可以使用 Builder
或使用方便的辅助方法创建字段:
FieldSpec android = FieldSpec.builder(String.class, "android")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC)
.addField(android)
.addField(String.class, "robot", Modifier.PRIVATE, Modifier.FINAL)
.build();
将生成:
public class HelloWorld {
private final String android;
private final String robot;
}
当字段具有 Javadoc 、注解或字段初始化器时,扩展 Builder
是必要的。字段初始化器使用与上面代码块相同的 String.format()
类语法:
FieldSpec android = FieldSpec.builder(String.class, "android")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.initializer("$S + $L", "Lollipop v.", 5.0d)
.build();
将生成:
private final String android = "Lollipop v." + 5.0;
接口
JavaPoet 生成接口也是没有问题的。注意,接口方法必须始终是 PUBLIC ABSTRACT
,接口字段必须始终是 PUBLIC STATIC FINAL
。这些修饰符在定义接口时是必要的:
TypeSpec helloWorld = TypeSpec.interfaceBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC)
.addField(FieldSpec.builder(String.class, "ONLY_THING_THAT_IS_CONSTANT")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.initializer("$S", "change")
.build())
.addMethod(MethodSpec.methodBuilder("beep")
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.build())
.build();
但是这些修饰符在生成代码时被省略。这些是默认设置,所以得益于 javac
,我们不需要包含它们!
public interface HelloWorld {
String ONLY_THING_THAT_IS_CONSTANT = "change";
void beep();
}
枚举
使用 enumBuilder
创建枚举类型,并为每个值添加 addEnumConstant()
:
TypeSpec helloWorld = TypeSpec.enumBuilder("Roshambo")
.addModifiers(Modifier.PUBLIC)
.addEnumConstant("ROCK")
.addEnumConstant("SCISSORS")
.addEnumConstant("PAPER")
.build();
将生成:
public enum Roshambo {
ROCK,
SCISSORS,
PAPER
}
支持复杂枚举,其中枚举值覆盖方法或调用超类构造函数。下面是一个综合的例子:
TypeSpec helloWorld = TypeSpec.enumBuilder("Roshambo")
.addModifiers(Modifier.PUBLIC)
.addEnumConstant("ROCK", TypeSpec.anonymousClassBuilder("$S", "fist")
.addMethod(MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addStatement("return $S", "avalanche!")
.returns(String.class)
.build())
.build())
.addEnumConstant("SCISSORS", TypeSpec.anonymousClassBuilder("$S", "peace")
.build())
.addEnumConstant("PAPER", TypeSpec.anonymousClassBuilder("$S", "flat")
.build())
.addField(String.class, "handsign", Modifier.PRIVATE, Modifier.FINAL)
.addMethod(MethodSpec.constructorBuilder()
.addParameter(String.class, "handsign")
.addStatement("this.$N = $N", "handsign", "handsign")
.build())
.build();
将生成:
public enum Roshambo {
ROCK("fist") {
@Override
public String toString() {
return "avalanche!";
}
},
SCISSORS("peace"),
PAPER("flat");
private final String handsign;
Roshambo(String handsign) {
this.handsign = handsign;
}
}
匿名内部类
在枚举代码中,我们使用了 TypeSpec.anonymousInnerClass()
,匿名内部类也可以用在代码块中,它们是可以用 $L
引用的值:
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();
这将生成一个方法,其中包含一个包含方法的类:
void sortByLength(List<String> strings) {
Collections.sort(strings, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
}
定义匿名内部类的一个特别棘手的部分是超类构造函数的参数。在上面的代码中,我们传递了没有参数的空字符串: TypeSpec.anonymousClassBuilder("")
。要传递不同的参数,请使用带有逗号的 JavaPoet 代码块语法来分隔参数。
注解
简单的注解很容易编写:
MethodSpec toString = MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.returns(String.class)
.addModifiers(Modifier.PUBLIC)
.addStatement("return $S", "Hoverboard")
.build();
它用 @Override
注解生成这个方法:
@Override
public String toString() {
return "Hoverboard";
}
使用 AnnotationSpec.builder()
在注解上设置属性:
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();
将会生成带有 accept
和 userAgent
属性的注解:
@Headers(
accept = "application/json; charset=utf-8",
userAgent = "Square Cash"
)
LogReceipt recordEvent(LogRecord logRecord);
注解值可以是注解本身,使用 $L
嵌入注解:
MethodSpec logRecord = MethodSpec.methodBuilder("recordEvent")
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.addAnnotation(AnnotationSpec.builder(HeaderList.class)
.addMember("value", "$L", AnnotationSpec.builder(Header.class)
.addMember("name", "$S", "Accept")
.addMember("value", "$S", "application/json; charset=utf-8")
.build())
.addMember("value", "$L", AnnotationSpec.builder(Header.class)
.addMember("name", "$S", "User-Agent")
.addMember("value", "$S", "Square Cash")
.build())
.build())
.addParameter(LogRecord.class, "logRecord")
.returns(LogReceipt.class)
.build();
将生成:
@HeaderList({
@Header(name = "Accept", value = "application/json; charset=utf-8"),
@Header(name = "User-Agent", value = "Square Cash")
})
LogReceipt recordEvent(LogRecord logRecord);
注意,可以使用相同的属性名多次调用 addMember()
来填充该属性的值列表。
Javadoc
可以用 Javadoc 记录字段、方法和类型:
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);
在引用 Javadoc 中的类型时使用 $T
来获得自动导入。
转载自:https://juejin.cn/post/6844904022600597517