2016-12-07 | work

使用 Java Poet 生成 Retrofit API 接口

JavaPoet: 一个用来生成 java 源文件的 library
Jackson: 解析 jaon

前段时间在重写公司应用的网络请求部分,iOS 那边需要上 https,趁这个机会把所有的网络请求统一用 Retrofit 实现。不过几十个接口改起来可不容易,幸好接口的文档还算完善,自己写了一个小程序根据文档生成 Retrofit 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 虚构的登陆接口
## URL: /api_version/path/login
## Method: POST
## Request:
| 参数名 | 参数类型 | 参数说明 | 是否必须 | 备注 | 示例 |
| :-- | :-- | :-- | :-- | :-- | :-- |
| username | String | 用户名 | 是 | | sunshine |
| password | String | 密码 | 是 | | sunshine |
## Response:
```javascript
{
"res":"0",
"data":"qweru9"//resetKey
}
```
## res code
| res | 说明 |
| --: | :-- |
| 0 | 成功 |
| 100 | 密码不正确 |

大概步骤就是读取接口相关的信息,再使用 java poet 拼接。URL 和 METHOD 直接获取就好。参数列表写了个正则提取出表格部分,然后比较暴力直接从第三行开始解析参数和类型,顺便做了一步和URL的比对,将路径参数使用@Path注解标记。

方法签名部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public MethodSpec generate(ClassName result) {
MethodSpec.Builder builder = MethodSpec.methodBuilder(apiName)
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.returns(ParameterizedTypeName.get(ClassName.get(Observable.class), result));

params.stream()
//过滤掉 token 相关 param
.filter(Param::filterToken)
.forEach(param -> {
ParameterSpec.Builder paramBuilder = ParameterSpec.builder(Utils.getType(param.type), param.name);
AnnotationSpec.Builder queryBuilder = null;

if (isUrlParam(param.name)) {
queryBuilder = AnnotationSpec.builder(Path.class);
} else {
// 这里图省事直接按照 requestType 分配 `@Query` 或 `@Field` 注解
switch (requestType) {
case "GET":
queryBuilder = AnnotationSpec.builder(Query.class);
break;
case "POST":
queryBuilder = AnnotationSpec.builder(Field.class);
break;
}
}

queryBuilder.addMember("value", "\"" + param.name + "\"");
paramBuilder.addAnnotation(queryBuilder.build());

builder.addParameter(paramBuilder.build());
});

AnnotationSpec.Builder methodAnnotation = null;
switch (requestType) {
case "GET":
methodAnnotation = AnnotationSpec.builder(GET.class);
break;
case "POST":
builder.addAnnotation(FormUrlEncoded.class);
methodAnnotation = AnnotationSpec.builder(POST.class);
break;
}

methodAnnotation.addMember("value", "\"" + url + "\"");

builder.addAnnotation(methodAnnotation.build());

return builder.build();
}

返回值的解析

返回值部分直接用 jackson 解析,递归判断下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

private TypeSpec buildJavaModel(JsonNode node, String name, String packageName) {

Builder jsonSpec = TypeSpec.classBuilder(name);
jsonSpec.addModifiers(Modifier.PUBLIC);

node.fieldNames().forEachRemaining(fieldName -> {
JsonNode item = node.get(fieldName);

if (fieldName.equals("res")) {
jsonSpec.superclass(ClassName.get(PACKAGE_NAME, "Res"));
} else {
if (item.isArray() && item.get(0) != null) {
ClassName list = ClassName.get("java.util", "List");
JsonNode listItem = item.get(0);
TypeSpec spec = buildJavaModel(listItem, Utils.upperFirstChar(name) + "Item", packageName);
TypeName listOfValue = ParameterizedTypeName.get(list, ClassName.get("", spec.name));
jsonSpec.addField(listOfValue, fieldName, Modifier.PUBLIC);
} else if (item.isObject()) {
TypeSpec typeSpec = buildJavaModel(item, name + Utils.upperFirstChar(fieldName), packageName);
jsonSpec.addField(ClassName.get(PACKAGE_NAME, typeSpec.name), fieldName, Modifier.PUBLIC);
} else {
jsonSpec.addField(Utils.getType(item), fieldName, Modifier.PUBLIC);
}
}
});

TypeSpec v = jsonSpec.build();
JavaFile javaFile = JavaFile.builder(packageName, v)
.build();

result = ClassName.get(packageName, v.name);

try {
javaFile.writeTo(Paths.get(Main.PARENT_DIR + "generate/"));
} catch (IOException e) {
e.printStackTrace();
}
return v;
}

七八十个老接口外加对应的 model 瞬间生成简直不能更爽,当然还有 model 复用和一些细节部分没有写的很完善,实际修改代码时改过来就好,工作量不大。