如何打造一个Dubbo网关--文档生成

上文我们分析了Maven插件作用,并知道了如何配置插件的各种参数,插件将完成如下几个目标:

  1. 使用Visitor遍历并记录AST
  2. 实现isCompressmakeJson功能
  3. 按接口保存文件并存在于最终生成的client-jar中
  4. 生成所有文件的索引和版本信息,这部分同样要在client-jar中

本篇内容主要讲述ASTNode和ASTVisitor,由于代码量较大只会挑部分说明。以下代码片段全部来自于plume

ASTNode说明

对于CompilationUnit我们定义一个ParseVisitor去访问,可能会出现的ASTNode有下面几种

  1. PackageDeclaration:包名
  2. ImportDeclaration:导入的类信息
  3. AnnotationTypeDeclaration:注解类,跳过
  4. EnumDeclaration:枚举类,跳过
  5. TypeDeclaration:类,里面包含父类、类泛型、方法、字段等
  6. MethodDeclaration:方法,包含方法泛型、参数、返回结果、修饰符等
  7. FieldDeclaration:字段,包含字段类型、修饰符等

除此之外,对于字段、方法参数、方法返回值、注解等的类型信息还需要一个TypeVisitor,里边会包含如下的ASTNode

  1. SimpleType:类型名,就是Class::getSimpleName。此种情况下需要补全:导入的类里如果有则使用导入类、否则使用包定义补全
  2. NameQualifiedType:类全名,不需要补全
  3. PrimitiveType:基本类型,当然不需要补全
  4. ArrayType:数组,需要更具具体类型判断
  5. UnionType:在捕获异常时使用|定义的多个类型,跳过,接口和Pojo里不会有
  6. ParameterizedType:泛型类型,这个是解析的难点
  7. MarkerAnnotation:无参数注解类型
  8. SingleMemberAnnotation:只有value,且不使用等号的注解类型
  9. NormalAnnotation:有等号的注解类型

对于注解的值,同样还需要ValueVisitor,里边会包含如下的ASTNode

  1. NumberLiteral:数字
  2. BooleanLiteral:布尔
  3. CharacterLiteral:字符
  4. StringLiteral:字符串
  5. NullLiteral:null
  6. ArrayInitializer:数组,花括号

因为类型信息需要补全,必须禁止*号导入,在IDEA下简单设置即可;不影响静态导入

类解析

ClassInfo是自定义的存储解析信息的类

infoImport是导入的类信息,用于补全;sourceInfo是最终的输出结果,一个文件可能有不止一个类,这里要用list

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
private Set<String> infoImport = new HashSet<>();
private List<ClassInfo> sourceInfo = new ArrayList<>();
private Stack<ClassInfo> infoStack = new Stack<>();

@Override
public boolean visit(TypeDeclaration node) {
final ClassInfo classInfo = new ClassInfo();
final String className = node.getName().getFullyQualifiedName();
if (filterSuffix.contains(classSuffix(className))) {
return false;
}
sourceInfo.add(classInfo);

// 按需要解析的对应长度依次入栈
if (node.isInterface()) {
// 是接口只解析方法
isInterface = true;
classInfo.setIfInterface(true);
for (int i = 0; i < node.getMethods().length; i++) {
infoStack.push(classInfo);
}
} else {
// 否则只解析字段
for (int i = 0; i < node.getFields().length; i++) {
infoStack.push(classInfo);
}
}

// 成员类或者静态类单独处理一下qualifiedName(类全名)
if (node.isPackageMemberTypeDeclaration()) {
final String qualifiedName = packageName + "." + className;
classInfo.setSimpleName(className);
classInfo.setQualifiedName(qualifiedName);
importClass.put(className, qualifiedName);
} else if (node.isMemberTypeDeclaration()) {
final TypeDeclaration parent = (TypeDeclaration) node.getParent();
final String parentName = parent.getName().getFullyQualifiedName();
final String qualifiedName = qualifiedName(parentName) + "." + className;
final String simpleName = simpleName(parentName) + "." + className;
classInfo.setSimpleName(simpleName);
classInfo.setQualifiedName(qualifiedName);
classInfo.setIfMember(true);
importClass.put(simpleName, qualifiedName);
} else {
classInfo.setQualifiedName(className);
}

// 如果是泛型类,需要设置泛型信息
final List typeParameters = node.typeParameters();
if (null != typeParameters && !typeParameters.isEmpty()) {
final List<String> generics = new ArrayList<>();
classInfo.setIfGeneric(true);
for (Object typeParameter : typeParameters) {
generics.add(((TypeParameter) typeParameter).getName().getFullyQualifiedName());
}
classInfo.setGenericList(generics);
}

// 读取当前父类信息,如果存在
final Set<TypeInfo> superclasses = new HashSet<>();
final Type superclass = node.getSuperclassType();
if (null != superclass) {
final TypeVisitor typeVisitor = new TypeVisitor();
superclass.accept(typeVisitor);
final TypeInfo typeInfo = typeVisitor.getTypeInfo();
superclasses.add(typeInfo);
//防止同包不存在
addImport(typeInfo.getQualifiedName(), infoImport);
}
classInfo.setSuperclass(superclasses);

// 可能存在的注解,作用于类的注解给作用于方法的同样注解一个默认值
final AnnotationInfoTuple annotationInfoTuple = annotationInfo(node.modifiers());
if (annotationInfoTuple.ifWhitelist) {
isWhitelist = true;
}
if (annotationInfoTuple.ifBackground) {
isBackground = true;
}
if (annotationInfoTuple.cacheTime > 0) {
cacheTime = annotationInfoTuple.cacheTime;
}
if (null != annotationInfoTuple.permissionInfo) {
permissionInfo = annotationInfoTuple.permissionInfo;
}
classInfo.setAnnotationInfo(annotationInfoTuple.annotationInfos);
// 可能存在的注释
classInfo.setCommentInfo(commentInfo(node.getJavadoc()));
return true;
}

这里解释一下为什么要定义了一个栈,主要是为了确保方法或字段绑定到正确的类上:

  • 首先解析按照源码编写顺序,从上到下一个个字符进行解析的
  • 一个源文件可能不止一个类声明,那么在碰到里面的成员类或者静态类声明时,之后的方法或字段必然属于这些类而不是最外层的公开类
  • 为了建立这些方法或字段到相对应classInfo的关系,使用栈是最简单的方案,栈是后进先出天然符合解析顺序
  • 碰到任何类声明时,将其中需要解析的方法或字段个数个classInfo实例入栈,在随后解析到方法或字段时出栈,栈中不存在这个类对应的classInfo时即表示这个类已经解析完成

注释解析

注释解析相对来说很简单,就是去解析Javadoc这个ASTNode,依次读取里边的标签并写入CommentInfo这个自定义的类里面

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
51
52
53
54
55
private CommentInfo commentInfo(Javadoc docs) {
if (null == docs || null == docs.tags()) {
return null;
}
final CommentInfo comment = new CommentInfo();
for (Object tag : docs.tags()) {
final TagElement tagElement = (TagElement) tag;
// 注释标签名
if (null == tagElement.getTagName()) {
// 不带任何标签的描述信息
comment.setDetail(tag2Text(tagElement));
} else {
switch (tagElement.getTagName().toLowerCase().trim()) {
case "@author":
// 作者
comment.setAuthor(tag2Text(tagElement));
break;
case "@title":
// 标题
comment.setTitle(tag2Text(tagElement));
break;
case "@time":
// 时间
comment.setTime(tag2Text(tagElement));
break;
case TagElement.TAG_PARAM:
// param可能会有多个
Map<String, String> param = comment.getParams();
if (null == param) {
param = new LinkedHashMap<>();
}
final List<String> paramList = tag2List(tagElement);
if (paramList.size() % 2 == 0) {
for (int i = 0; i < paramList.size() - 1; i += 2) {
param.put(paramList.get(i), paramList.get(i + 1));
}
} else {
param.put(paramList.get(0), "");
}
comment.setParams(param);
break;
case TagElement.TAG_RETURN:
// return只有一个
comment.setReturned(tag2Text(tagElement));
break;
case TagElement.TAG_EXCEPTION:
// 对受检异常的注释
comment.setException(tag2List(tagElement));
break;
default:
}
}
}
return comment;
}

注解解析

注解解析需要用到TypeVisitor,主要是去解析Annotation这个ASTNode

这段代码是用来处理Injection这个自定义注解;injectionTypeMojo的一个字段,可以传入,默认就是Injection的全名

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private AnnotationInfoTuple annotationParam(List list) {
final AnnotationInfoTuple annotationInfoPair = new AnnotationInfoTuple();
if (null == list || list.isEmpty()) {
return annotationInfoPair;
}

List<AnnotationInfo> annotationInfos = new ArrayList<>();
for (Object one : list) {
// 只处理ASTNode是Annotation的情况
if (one instanceof Annotation) {
final Annotation annotation = (Annotation) one;
// 因为内部情况比较复杂,使用TypeVisitor遍历
final TypeVisitor typeVisitor = new TypeVisitor();
annotation.accept(typeVisitor);
// 获取遍历后的信息,去拼装InjectionInfo
final AnnotationInfo annotationInfo = typeVisitor.getAnnotationInfo();
// 只处理@Injection
if (injectionType.equalsIgnoreCase(annotationInfo.getQualifiedName())) {
final InjectionInfo injectionInfo = new InjectionInfo();
// 注解的全部值信息
Map<String, Object> annotationValue = annotationInfo.getAnnotationValue();
if (null == annotationValue) {
annotationValue = new HashMap<>();
}

// 从一个自定义的工具类中读取InjectEnum的信息,之前说过会分成三个等级,还需要默认值
final Map<String, String> injectionEnum = getInjectionEnum(this.injectionEnum);
final Object value = annotationValue.get("value");
if (null == value) {
injectionInfo.setInjectType(injectionEnum.getOrDefault(InjectionUtil.DEFAULT_VALUE, ""));
injectionInfo.setInvokeName(injectionEnum.get(injectionInfo.getInjectType()));
} else {
injectionInfo.setInjectType(splitLastByDot(String.valueOf(value)));
injectionInfo.setInvokeName(injectionEnum.get(injectionInfo.getInjectType()));
}
// 解析附加的字段
final Object ip = annotationValue.get("ip");
if (null != ip) {
injectionInfo.setHaveAddress(Boolean.valueOf(String.valueOf(ip)));
}
final Object token = annotationValue.get("token");
if (null != token) {
injectionInfo.setNeedToken(Boolean.valueOf(String.valueOf(token)));
}
final Object headers = annotationValue.get("headers");
if (null != headers) {
injectionInfo.setHeaderNames(annotationSet(headers));
}
final Object cookies = annotationValue.get("cookies");
if (null != cookies) {
injectionInfo.setCookieNames(annotationSet(cookies));
}

annotationInfoPair.ifInjection = true;
annotationInfoPair.injectionInfo = injectionInfo;
}
if (useAnnotation) {
annotationInfos.add(annotationInfo);
}
}
}
annotationInfoPair.annotationInfos = annotationInfos;
return annotationInfoPair;
}

生成json和压缩

这一步比较简单,主要是为了满足配置的参数makeJsonisCompress

如前文所说makeJson主要是用于调试,所以会跳过压缩;isCompress默认开启,但在某些情况下也可以关闭

  • 默认使用Protostuff来序列化,开启makeJson将会使用Jackson序列化,同时pretty化json更易读
  • 压缩默认使用Snappy,同时在此版本中为了便于保存和传输,压缩后文件内会存储Base64字符串
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
/**
* 保存的类信息为文件
* 返回转换完成的byte数组用于计算版本
*
* @param sourceInfo 单个类信息,内部类也会单独算
* @param fullPath 会保存的路径
*/
private byte[] saveFile(Object sourceInfo, String fullPath) throws IOException {
log.debug("Create file: " + fullPath + ", content= " + sourceInfo);
try (final FileWriter writer = new FileWriter(fullPath)) {
byte[] data = Protostuff.toByte(sourceInfo);

if (makeJson) {
writer.write(Jackson.toPrettyJson(sourceInfo));
} else {
if (isCompress) {
final byte[] compress = Snappy.compress(data);
log.info("Compression Ratio: " + (int) ((compress.length / (double) data.length) * 10000) / 100.0);
data = compress;
}
writer.write(Base64.encode(data));
}
writer.flush();
return data;
}
}

生成索引和版本

  • 每一个类有一个文件和一个版本
  • 索引里包含解析的全部类和这些类对应的版本
  • 生成的全部文件,也就是解析的全部类也有一个版本,这个版本通过索引生成

所谓的版本是一个摘要,这里使用的MD5,只要说明当前计算范围内的对象有没有变化即可;稍后要根据是否有变化决定是否存储入数据库

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
/**
* 存储每个类的解析信息
*
* @param sourceInfo 解析后的类数据
* @param savePath target/classes/PARSE-INF/${systemId}
*/
private void saveParse(ClassInfo sourceInfo, String savePath) throws IOException {
if (null != sourceInfo && null != sourceInfo.getQualifiedName()) {
// 获取类的全名做为文件名
final String qualifiedName = sourceInfo.getQualifiedName();
log.info("Process File Success: " + qualifiedName);

final Path getPath = Paths.get(savePath);
if (!Files.exists(getPath)) {
Files.createDirectories(getPath);
}
final byte[] data = saveFile(sourceInfo, savePath + qualifiedName);
// 将单个文件的版本放入索引map(LinkedHashMap)
map.put(qualifiedName, ParseUtil.getVersion(data));
}
}

/**
* 存储索引
*
* @param savePath target/classes/PARSE-INF/${systemId}
*/
private void saveIndex(String savePath) throws IOException {
log.info("Process Index");
// 计算索引的版本,作为client-jar的版本
final String version = ParseUtil.getVersion(saveMap(map, savePath + indexName));
try (final FileWriter writer = new FileWriter(savePath + versionName)) {
writer.write(version);
writer.flush();
}
}

保存和使用文件

只要把生成的所有文件都放入${project.build.directory}的子目录即可

对于Maven来说默认是target/classes,在此基础上实际会放入target/classes/PARSE-INF/${systemId}。对应的读取目录就是classpath:/PARSE-INF/${systemId}

之所以使用systemId做为子目录是因为:client-jar肯定不止一个,那么每一个client-jar内都有这些信息,而在启动时读取classpath:/PARSE-INF下全部文件并初始化是绝对不可行的,原因有下

  1. 如果解析全部client-jar里的信息不仅会拖慢启动速度,而且数据量可能会很大,影响解析数据的上报
  2. 引入的其它项目的client-jar可能会落后好几个版本,所以绝对不能变更其它项目的解析数据

这里简单的提一下解析数据的初始化,需要引入一个独立的子系统,称为平台

  1. 每个业务系统启动时读取自己的classpath:/PARSE-INF/${systemId}下的文件
  2. 将读取到的文件组装成某种格式发送给平台,平台负责对比版本并决定是否存储(这里分为两步)
  3. 每个业务系统也可能会有多个client-jar,这些jar里的所有信息都会交给平台处理

如何打造一个Dubbo网关--文档生成
https://back.pub/post/hh-code-dubbo-gateway-5/
作者
Dash
发布于
2018年11月18日
许可协议