上文我们分析了Maven插件作用,并知道了如何配置插件的各种参数,插件将完成如下几个目标:
- 使用
Visitor
遍历并记录AST
- 实现
isCompress
和makeJson
功能
- 按接口保存文件并存在于最终生成的client-jar中
- 生成所有文件的索引和版本信息,这部分同样要在client-jar中
本篇内容主要讲述ASTNode和ASTVisitor,由于代码量较大只会挑部分说明。以下代码片段全部来自于plume
ASTNode说明
对于CompilationUnit
我们定义一个ParseVisitor
去访问,可能会出现的ASTNode有下面几种
- PackageDeclaration:包名
- ImportDeclaration:导入的类信息
- AnnotationTypeDeclaration:注解类,跳过
- EnumDeclaration:枚举类,跳过
- TypeDeclaration:类,里面包含父类、类泛型、方法、字段等
- MethodDeclaration:方法,包含方法泛型、参数、返回结果、修饰符等
- FieldDeclaration:字段,包含字段类型、修饰符等
除此之外,对于字段、方法参数、方法返回值、注解等的类型信息还需要一个TypeVisitor
,里边会包含如下的ASTNode
- SimpleType:类型名,就是Class::getSimpleName。此种情况下需要补全:导入的类里如果有则使用导入类、否则使用包定义补全
- NameQualifiedType:类全名,不需要补全
- PrimitiveType:基本类型,当然不需要补全
- ArrayType:数组,需要更具具体类型判断
- UnionType:在捕获异常时使用
|
定义的多个类型,跳过,接口和Pojo里不会有
- ParameterizedType:泛型类型,这个是解析的难点
- MarkerAnnotation:无参数注解类型
- SingleMemberAnnotation:只有value,且不使用等号的注解类型
- NormalAnnotation:有等号的注解类型
对于注解的值,同样还需要ValueVisitor
,里边会包含如下的ASTNode
- NumberLiteral:数字
- BooleanLiteral:布尔
- CharacterLiteral:字符
- StringLiteral:字符串
- NullLiteral:null
- 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); } }
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: 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: comment.setReturned(tag2Text(tagElement)); break; case TagElement.TAG_EXCEPTION: comment.setException(tag2List(tagElement)); break; default: } } } return comment; }
|
注解解析
注解解析需要用到TypeVisitor
,主要是去解析Annotation
这个ASTNode
这段代码是用来处理Injection
这个自定义注解;injectionType
是Mojo
的一个字段,可以传入,默认就是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) { if (one instanceof Annotation) { final Annotation annotation = (Annotation) one; final TypeVisitor typeVisitor = new TypeVisitor(); annotation.accept(typeVisitor); final AnnotationInfo annotationInfo = typeVisitor.getAnnotationInfo(); if (injectionType.equalsIgnoreCase(annotationInfo.getQualifiedName())) { final InjectionInfo injectionInfo = new InjectionInfo(); Map<String, Object> annotationValue = annotationInfo.getAnnotationValue(); if (null == annotationValue) { annotationValue = new HashMap<>(); }
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和压缩
这一步比较简单,主要是为了满足配置的参数makeJson
和isCompress
如前文所说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
|
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
|
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.put(qualifiedName, ParseUtil.getVersion(data)); } }
private void saveIndex(String savePath) throws IOException { log.info("Process Index"); 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
下全部文件并初始化是绝对不可行的,原因有下
- 如果解析全部client-jar里的信息不仅会拖慢启动速度,而且数据量可能会很大,影响解析数据的上报
- 引入的其它项目的client-jar可能会落后好几个版本,所以绝对不能变更其它项目的解析数据
这里简单的提一下解析数据的初始化,需要引入一个独立的子系统,称为平台
- 每个业务系统启动时读取自己的
classpath:/PARSE-INF/${systemId}
下的文件
- 将读取到的文件组装成某种格式发送给平台,平台负责对比版本并决定是否存储(这里分为两步)
- 每个业务系统也可能会有多个client-jar,这些jar里的所有信息都会交给平台处理