背景
spring cloud多个微服务开发了很多接口,紧急对接前端,需要快速提供一批接口的文档,且不同微服务的接口由多位同事开发且注释非常的少各有不同,现在需要不修改代码不添加注释的情况下能自动的扫描接口并生成文档。本文将详细介绍实现此需求的技术方案。
技术方案
在通过网络搜索后,最终定位到了JApiDocs开源代码,感谢大神开源,此代码基本实现了我想要的,但是需要对源码做些改动。
-
JApiDocs源码:https://github.com/YeDaxia/JApiDocs
-
JApiDocs详细简介:https://japidocs.agilestudio.cn/#/zh-cn/
-
源码弊端
- 只扫描class中的接口,不生成interface中的接口
- 只扫描@xxxxController注解的java文件,我还需要扫描自己实现注解@PaaFeignClient的java文件
- 接口参数使用的方法注释@param中的,我的很多接口没有注释
- 接口名使用的方法名,不够直观
- @RequestParam中未添加is_required的场景spring默认为true,这里有bug
- 不支持参数中default的支持
- 参数名未优先使用@RequestParma中的name或value
- 获取接口type存在bug
-
我想要的
- 扫描java文件规则
- 扫描带有@ResetController或@PaasFeignClient的class或interface文件
- 扫描要生成的接口规则
- 扫描带有@RequestMapping注解的方法
- 接口名使用@RequestMapping的name或value
- url使用类上@RequestMapping的name或value 和 方法上@RequestMapping的name或value的拼装,在最前面自动拼装了微服务名
- 接口注释采用接口方法注释中的description内容
- 扫描接口参数规则
- 扫描带有@RequstParam参数
- 参数名使用@RequstParam的name或value
- 参数是否必须采用@RequstParam的is_required,没有跟spring保持一致默认必须
- 参数默认值采用@RequstParam的default值,没有为空
- 参数注释采用接口方法注释@param中的description
- 参数作者采用类注释中的@author
- 接口返回值
- 自动解析返回值PaasResult,如果PaasResult声明返回格式,会自动解析对象,所以最好是声明返回格式
- 扫描java文件规则
JApiDocs使用方法
- POM加入依赖
<dependency>
<groupId>io.github.yedaxia</groupId>
<artifactId>japidocs</artifactId>
<version>1.4.3</version>
</dependency>
- 可以在test里添加生成doc代码,这样每次构建都会自动生成接口文档
@Test
public void jApiDocTest() throws FileNotFoundException {
String projectPath = new File(ResourceUtils.getURL("../").getPath()).getAbsolutePath();
String docPath = new File(ResourceUtils.getURL("../APP-META/doc/all").getPath()).getAbsolutePath();
System.out.println("projectPath=" + projectPath);
System.out.println("docPath=" + docPath);
DocsConfig config = new DocsConfig();
config.setProjectPath(projectPath); // 项目根目录
config.setProjectName("paas"); // 项目名称
config.setApiVersion("V1.0"); // 声明该API的版本
config.setDocsPath(docPath); // 生成API 文档所在目录
config.setMvcFramework("spring");
//config.addJavaSrcPath("E:\\github\\workspace\\paas\\paas-app-ops\\src\\");
config.setAutoGenerate(Boolean.TRUE); // 配置自动生成
config.addPlugin(new MarkdownDocPlugin());
Docs.buildHtmlDocs(config); // 执行生成文档
}
- 正如JApiDocs文档所言,到这里你几乎就可以生成文档了
源码改动
针对上面所说的弊端或者与我需求不同的点,下面是我对源码的改动
实现源码改动不需要下载源码,只需要在自己module中重新实现要改动的相关文件就好了
- DocContext.java 添加对其他注解的类支持
case SPRING:
controllerParser = new SpringControllerParser();
Utils.wideSearchFile(javaSrcDir, (f, name) -> f.getName().endsWith(".java") && ParseUtils.compilationUnit(f)
.getChildNodesByType(ClassOrInterfaceDeclaration.class)
.stream()
.anyMatch(cd -> (cd.getAnnotationByName("Controller").isPresent()
|| cd.getAnnotationByName("RestController").isPresent()
|| cd.getAnnotationByName("PaasFeignClient").isPresent())
&& !cd.getAnnotationByName(Ignore.class.getSimpleName()).isPresent())
, result, false);
controllerFiles.addAll(result);
break;
- RequestNode.java 添加属性 interfaceName
- ParamNode.java添加属性defaultStr
- AbsControllerParser.java
- parse中添加对interface的支持
public ControllerNode parse(File javaFile) { this.javaFile = javaFile; this.compilationUnit = ParseUtils.compilationUnit(javaFile); this.controllerNode = new ControllerNode(); String controllerName = Utils.getJavaFileName(javaFile); controllerNode.setClassName(controllerName); compilationUnit.getClassByName(controllerName) .ifPresent(c -> { beforeHandleController(controllerNode, c); parseClassDoc(c); parseMethodDocs(c); afterHandleController(controllerNode, c); }); if (controllerName.contains("Interface")) { compilationUnit.getInterfaceByName(controllerName) .ifPresent(c -> { beforeHandleController(controllerNode, c); parseClassDoc(c); parseMethodDocs(c); afterHandleController(controllerNode, c); }); } return controllerNode; }
- parseMethodDocs 方法 添加requestNode新属性
private void parseMethodDocs(ClassOrInterfaceDeclaration c) { c.findAll(MethodDeclaration.class).stream() //.filter(m -> m.getModifiers().contains(Modifier.PUBLIC)) .forEach(m -> { boolean existsApiDoc = m.getAnnotationByName(ApiDoc.class.getSimpleName()).isPresent(); if (!existsApiDoc && !controllerNode.getGenerateDocs() && !DocContext.getDocsConfig().getAutoGenerate()) { return; } if(shouldIgnoreMethod(m)){ return; } RequestNode requestNode = new RequestNode(); requestNode.setControllerNode(controllerNode); requestNode.setAuthor(controllerNode.getAuthor()); requestNode.setMethodName(m.getNameAsString()); requestNode.setUrl(requestNode.getMethodName()); requestNode.setDescription(requestNode.getMethodName()); requestNode.setInterfaceName(requestNode.getMethodName()); m.getAnnotationByClass(Deprecated.class).ifPresent(f -> { requestNode.setDeprecated(true); }); m.getParameters().forEach(p -> { /* p.getAnnotationByName("RequestParam").ifPresent(f -> { ParamNode paramNode = new ParamNode(); // @RequestParam("email") String email if (f instanceof SingleMemberAnnotationExpr) { paramNode.setName(((StringLiteralExpr) ((SingleMemberAnnotationExpr) f).getMemberValue()).getValue()); return; } // @RequestParam(name = "email", required = true) if (f instanceof NormalAnnotationExpr) { ((NormalAnnotationExpr) f).getPairs().forEach(pair -> { String exprName = pair.getNameAsString(); if ("value".equals(exprName) || "name".equals(exprName)) { String exprValue = ((StringLiteralExpr) pair.getValue()).getValue(); paramNode.setName(exprValue); } }); } requestNode.addParamNode(paramNode); });*/ ParamNode paramNode = new ParamNode(); paramNode.setName(p.getNameAsString()); paramNode.setDefaultStr(" "); requestNode.addParamNode(paramNode); }); m.getJavadoc().ifPresent(d -> { String description = d.getDescription().toText(); requestNode.setDescription(description); List<JavadocBlockTag> blockTagList = d.getBlockTags(); for (JavadocBlockTag blockTag : blockTagList) { if (blockTag.getTagName().equalsIgnoreCase("param")) { ParamNode paramNode = requestNode.getParamNodeByName(blockTag.getName().get()); if (paramNode != null) { paramNode.setDescription(blockTag.getContent().toText()); //requestNode.addParamNode(paramNode); } } else if (blockTag.getTagName().equalsIgnoreCase("author")) { requestNode.setAuthor(blockTag.getContent().toText()); } else if(blockTag.getTagName().equalsIgnoreCase("description")){ requestNode.setSupplement(blockTag.getContent().toText()); } } }); m.getParameters().forEach(p -> { String paraName = p.getName().asString(); ParamNode paramNode = requestNode.getParamNodeByName(paraName); if (paramNode != null && ParseUtils.isExcludeParam(p)) { requestNode.getParamNodes().remove(paramNode); return; } if (paramNode != null) { Type pType = p.getType(); boolean isList = false; if(pType instanceof ArrayType){ isList = true; pType = ((ArrayType) pType).getComponentType(); }else if(ParseUtils.isCollectionType(pType.asString())){ List<ClassOrInterfaceType> collectionTypes = pType.getChildNodesByType(ClassOrInterfaceType.class); isList = true; if(!collectionTypes.isEmpty()){ pType = collectionTypes.get(0); }else{ paramNode.setType("Object[]"); } }else{ pType = p.getType(); } if(paramNode.getType() == null){ if(ParseUtils.isEnum(getControllerFile(), pType.asString())){ paramNode.setType(isList ? "enum[]": "enum"); }else{ final String pUnifyType = ParseUtils.unifyType(pType.asString()); paramNode.setType(isList ? pUnifyType + "[]": pUnifyType); } } } }); com.github.javaparser.ast.type.Type resultClassType = null; String stringResult = null; if (existsApiDoc) { AnnotationExpr an = m.getAnnotationByName("ApiDoc").get(); if (an instanceof SingleMemberAnnotationExpr) { resultClassType = ((ClassExpr) ((SingleMemberAnnotationExpr) an).getMemberValue()).getType(); } else if (an instanceof NormalAnnotationExpr) { for (MemberValuePair pair : ((NormalAnnotationExpr) an).getPairs()) { final String pairName = pair.getNameAsString(); if ("result".equals(pairName) || "value".equals(pairName)) { resultClassType = ((ClassExpr) pair.getValue()).getType(); } else if (pairName.equals("url")) { requestNode.setUrl(((StringLiteralExpr) pair.getValue()).getValue()); } else if (pairName.equals("method")) { requestNode.addMethod(((StringLiteralExpr) pair.getValue()).getValue()); } else if("stringResult".equals(pairName)){ stringResult = ((StringLiteralExpr)pair.getValue()).getValue(); } } } } afterHandleMethod(requestNode, m); if (resultClassType == null) { if (m.getType() == null) { return; } resultClassType = m.getType(); } ResponseNode responseNode = new ResponseNode(); responseNode.setRequestNode(requestNode); if(stringResult != null){ responseNode.setStringResult(stringResult); }else{ handleResponseNode(responseNode, resultClassType.getElementType(), javaFile); } requestNode.setResponseNode(responseNode); setRequestNodeChangeFlag(requestNode); controllerNode.addRequestNode(requestNode); }); }
- SpringControllerParser.java
- afterHandleMethod主要是对注解的解析
protected void afterHandleMethod(RequestNode requestNode, MethodDeclaration md) { md.getAnnotations().forEach(an -> { String name = an.getNameAsString(); if (Arrays.asList(MAPPING_ANNOTATIONS).contains(name)) { String method = Utils.getClassName(name).toUpperCase().replace("MAPPING", ""); if (!"REQUEST".equals(method)) { requestNode.addMethod(RequestMethod.valueOf(method).name()); } if (an instanceof NormalAnnotationExpr) { ((NormalAnnotationExpr) an).getPairs().forEach(p -> { String key = p.getNameAsString(); if (isUrlPathKey(key)) { requestNode.setUrl(Utils.removeQuotations(p.getValue().toString())); requestNode.setInterfaceName(Utils.removeQuotations(p.getValue().toString())); } if ("headers".equals(key)) { Expression methodAttr = p.getValue(); if (methodAttr instanceof ArrayInitializerExpr) { NodeList<Expression> values = ((ArrayInitializerExpr) methodAttr).getValues(); for (Node n : values) { String[] h = n.toString().split("="); requestNode.addHeaderNode(new HeaderNode(h[0], h[1])); } } else { String[] h = p.getValue().toString().split("="); requestNode.addHeaderNode(new HeaderNode(h[0], h[1])); } } if ("method".equals(key)) { Expression methodAttr = p.getValue(); if (methodAttr instanceof ArrayInitializerExpr) { NodeList<Expression> values = ((ArrayInitializerExpr) methodAttr).getValues(); for (Node n : values) { requestNode.addMethod(RequestMethod.valueOf(Utils.getClassName(n.toString())).name()); } } else { requestNode.addMethod(RequestMethod.valueOf(Utils.getClassName(p.getValue().toString())).name()); } } }); } if (an instanceof SingleMemberAnnotationExpr) { String url = ((SingleMemberAnnotationExpr) an).getMemberValue().toString(); requestNode.setInterfaceName(Utils.removeQuotations(url)); requestNode.setUrl(Utils.removeQuotations(url)); requestNode.addMethod("GET"); } // add service name String packageName = getControllerNode().getPackageName(); String preFix = ""; if (packageName.contains("meta")) { preFix = "/meta/"; } else if (packageName.contains("rm")) { preFix = "/rm/"; } else if (packageName.contains("ops")) { preFix = "/ops/"; } requestNode.setUrl(Utils.getActionUrl(getControllerNode().getBaseUrl(), preFix + requestNode.getUrl())); } }); md.getParameters().forEach(p -> { String paraName = p.getName().asString(); ParamNode paramNode = requestNode.getParamNodeByName(paraName); if (paramNode != null) { p.getAnnotations().forEach(an -> { String name = an.getNameAsString(); // @NotNull, @NotBlank, @NotEmpty if (ParseUtils.isNotNullAnnotation(name)) { paramNode.setRequired(true); return; } if (!"RequestParam".equals(name) && !"RequestBody".equals(name) && !"PathVariable".equals(name)) { return; } if ("RequestBody".equals(name)) { setRequestBody(paramNode, p.getType()); } // @RequestParam String name if (an instanceof MarkerAnnotationExpr) { paramNode.setRequired(true); return; } // @RequestParam("email") String email if (an instanceof SingleMemberAnnotationExpr) { paramNode.setRequired(Boolean.TRUE); paramNode.setName(((StringLiteralExpr) ((SingleMemberAnnotationExpr) an).getMemberValue()).getValue()); return; } // @RequestParam(name = "email", required = true) if (an instanceof NormalAnnotationExpr) { boolean required_flag = false; for (MemberValuePair pair : ((NormalAnnotationExpr) an).getPairs()) { String exprName = pair.getNameAsString(); if ("required".equals(exprName)) { required_flag = true; Boolean exprValue = ((BooleanLiteralExpr) pair.getValue()).getValue(); paramNode.setRequired(Boolean.valueOf(exprValue)); } else if ("value".equals(exprName) || "name".equals(exprName)) { String exprValue = ((StringLiteralExpr) pair.getValue()).getValue(); paramNode.setName(exprValue); } else if ("defaultValue".equals(exprName)) { String exprValue = ((StringLiteralExpr) pair.getValue()).getValue(); paramNode.setDefaultStr(exprValue); } } // @RequestParam(name = "email") 省略require的场景 if (!required_flag) { paramNode.setRequired(Boolean.TRUE); } } }); //如果参数是个对象 if (!paramNode.isJsonBody() && ParseUtils.isModelType(paramNode.getType())) { ClassNode classNode = new ClassNode(); ParseUtils.parseClassNodeByType(getControllerFile(), classNode, p.getType()); List<ParamNode> paramNodeList = new ArrayList<>(); toParamNodeList(paramNodeList, classNode, ""); requestNode.getParamNodes().remove(paramNode); requestNode.getParamNodes().addAll(paramNodeList); } } }); // add action param ParamNode paramNode = new ParamNode(); paramNode.setName("Action"); paramNode.setType("string"); paramNode.setRequired(true); paramNode.setDescription("固定值:" + requestNode.getInterfaceName()); paramNode.setDefaultStr(" "); requestNode.addParamNode(0, paramNode); }
配置文件
效果
-
md文件输出如下
-
html文件输出如下