背景
spring cloud多个微服务开发了很多接口,紧急对接前端,需要快速提供一批接口的文档,且不同微服务的接口由多位同事开发且注释非常的少各有不同,现在需要不修改代码不添加注释的情况下能自动的扫描接口并生成文档。本文将详细介绍实现此需求的技术方案。
技术方案
在通过网络搜索后,最终定位到了JApiDocs开源代码,感谢大神开源,此代码基本实现了我想要的,但是需要对源码做些改动。
JApiDocs使用方法
<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
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文件输出如下