├── doc ├── doc1.png └── doc2.png ├── todo.txt ├── src └── main │ └── java │ └── com │ └── github │ └── chuanzh │ ├── config │ ├── TemplateConfig.java │ └── GlobalConfig.java │ ├── engine │ ├── AbstractTemplateEngine.java │ └── FreemarkerTemplateEngine.java │ ├── enums │ ├── TemplateType.java │ └── RequestMode.java │ ├── po │ ├── Response.java │ ├── Request.java │ ├── ControllerInfo.java │ └── MethodInfo.java │ ├── util │ ├── ClassHelperUtils.java │ └── ControllerInfoBuilder.java │ └── DocAutoGenerator.java ├── .gitignore ├── templates ├── markdown.ftl └── html.ftl ├── README.md └── pom.xml /doc/doc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chuanzh/document-genteration/HEAD/doc/doc1.png -------------------------------------------------------------------------------- /doc/doc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chuanzh/document-genteration/HEAD/doc/doc2.png -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | 1. 请求参数为Long、String、int等基本类型时,需要单独处理 2 | 1.1. 已支持markdown,但在项目中引入maven依赖会出现freemarker模板找不到的情况 3 | 1.2. 返回对象Data数据属性未解析出来 4 | 2. 支持多模板的方式,如markdown(默认)、word 5 | 3. 支持文档生成方式为:整合(一个文件)、拆分(每个controller一个文件) 6 | -------------------------------------------------------------------------------- /src/main/java/com/github/chuanzh/config/TemplateConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.chuanzh.config; 2 | 3 | /** 4 | * @author zhangchuan 5 | */ 6 | public class TemplateConfig { 7 | 8 | public static final String MARK_DOWN_TEMPLATE = "/templates/markdown.ftl"; 9 | public static final String HTML_TEMPLATE = "/templates/html.ftl"; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/github/chuanzh/engine/AbstractTemplateEngine.java: -------------------------------------------------------------------------------- 1 | package com.github.chuanzh.engine; 2 | 3 | import java.util.Map; 4 | 5 | public abstract class AbstractTemplateEngine { 6 | 7 | public abstract AbstractTemplateEngine init(); 8 | 9 | public abstract AbstractTemplateEngine process(Map params); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/chuanzh/enums/TemplateType.java: -------------------------------------------------------------------------------- 1 | package com.github.chuanzh.enums; 2 | 3 | public enum TemplateType { 4 | 5 | MARKDOWN("md", "markdown文档格式"); 6 | //HTML("html", "html文档格式"); 7 | 8 | private String type; 9 | private String desc; 10 | 11 | private TemplateType(String type, String desc) { 12 | this.type = type; 13 | this.desc = desc; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/chuanzh/enums/RequestMode.java: -------------------------------------------------------------------------------- 1 | package com.github.chuanzh.enums; 2 | /** 3 | * @author zhangchuan 4 | */ 5 | public enum RequestMode { 6 | 7 | POST("POST","POST请求"), 8 | GET("GET","GET请求"), 9 | GET_OR_POST("GET|POST","GET或POST请求"); 10 | 11 | private String mode; 12 | private String desc; 13 | 14 | private RequestMode(String mode, String desc) { 15 | this.mode = mode; 16 | this.desc = desc; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | 33 | .mvn/ 34 | mvnw 35 | mvnw.cmd 36 | 37 | .DS_Store 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/chuanzh/po/Response.java: -------------------------------------------------------------------------------- 1 | package com.github.chuanzh.po; 2 | /** 3 | * @author zhangchuan 4 | */ 5 | public class Response { 6 | 7 | private String title; 8 | private String name; 9 | private String type; 10 | private String desc; 11 | 12 | public String getTitle() { 13 | return title; 14 | } 15 | 16 | public void setTitle(String title) { 17 | this.title = title; 18 | } 19 | 20 | public String getName() { 21 | return name; 22 | } 23 | public void setName(String name) { 24 | this.name = name; 25 | } 26 | public String getType() { 27 | return type; 28 | } 29 | public void setType(String type) { 30 | this.type = type; 31 | } 32 | public String getDesc() { 33 | return desc; 34 | } 35 | public void setDesc(String desc) { 36 | this.desc = desc; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/github/chuanzh/po/Request.java: -------------------------------------------------------------------------------- 1 | package com.github.chuanzh.po; 2 | 3 | /** 4 | * @author zhangchuan 5 | */ 6 | public class Request { 7 | 8 | private String title; 9 | private String name; 10 | private String type; 11 | private int isNotNull; 12 | private String desc; 13 | 14 | public String getTitle() { 15 | return title; 16 | } 17 | 18 | public void setTitle(String title) { 19 | this.title = title; 20 | } 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | public void setName(String name) { 26 | this.name = name; 27 | } 28 | public String getType() { 29 | return type; 30 | } 31 | public void setType(String type) { 32 | this.type = type; 33 | } 34 | public String getDesc() { 35 | return desc; 36 | } 37 | public void setDesc(String desc) { 38 | this.desc = desc; 39 | } 40 | public int getIsNotNull() { 41 | return isNotNull; 42 | } 43 | public void setIsNotNull(int isNotNull) { 44 | this.isNotNull = isNotNull; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/github/chuanzh/po/ControllerInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.chuanzh.po; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * @author zhangchuan 7 | */ 8 | public class ControllerInfo { 9 | 10 | 11 | /** 12 | * 请求url 13 | */ 14 | private String requestUrl; 15 | 16 | /** 17 | * 请求描述 18 | */ 19 | private String desc; 20 | 21 | /** 22 | * 所有的方法集合 23 | */ 24 | private List methodInfoList; 25 | 26 | 27 | public String getRequestUrl() { 28 | return requestUrl; 29 | } 30 | 31 | public void setRequestUrl(String requestUrl) { 32 | this.requestUrl = requestUrl; 33 | } 34 | 35 | public String getDesc() { 36 | return desc; 37 | } 38 | 39 | public void setDesc(String desc) { 40 | this.desc = desc; 41 | } 42 | 43 | public List getMethodInfoList() { 44 | return methodInfoList; 45 | } 46 | 47 | public void setMethodInfoList(List methodInfoList) { 48 | this.methodInfoList = methodInfoList; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /templates/markdown.ftl: -------------------------------------------------------------------------------- 1 | 2 | # ${title}接口文档 3 | 4 | <#list interfaceDetails as interfaceDetail> 5 | ### ${interfaceDetail_index+1!}. ${interfaceDetail["title"]!} 6 | ${interfaceDetail["requestType"]!} 7 | 8 | ##### 请求参数 9 | <#list interfaceDetail["request"]?keys as key> 10 | <#if key_index == 0> 11 | <#else> 12 | **${key!}** 13 | 14 | |字段名称|类型|是否必填|说明| 15 | |----- |-------|-----|----- | 16 | <#list interfaceDetail["request"][key] as request> 17 | |${request.name!}|${request.type!}|<#if request.isNotNull == 0>否<#else>是|${request.desc!}| 18 | 19 | 20 | 21 | ##### 响应参数 22 | <#list interfaceDetail["response"]?keys as key> 23 | <#if key_index==0> 24 | <#else> 25 | **${key!}** 26 | 27 | |字段名称|类型|说明 | 28 | |----- |------|----------------------------- | 29 | <#list interfaceDetail["response"][key] as response> 30 | |${response.name!}|${response.type!}|${response.desc!}| 31 | 32 | 33 | 34 | ##### 请求示例 35 | # 36 | ``` javascript 37 | { 38 | 39 | } 40 | ``` 41 | 42 | ##### 响应示例 43 | # 44 | ``` javascript 45 | { 46 | "code": 10000, 47 | "message": "成功" 48 | } 49 | ``` 50 | 51 | 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # document-genteration 2 | 基于Swagger自动生成接口文档,生成格式为MarkDown,后续会支持更多格式 3 | 4 | # 使用方法 5 | ## 添加maven依赖 6 | ```xml 7 | 8 | com.github.chuanzh 9 | document-generation 10 | 1.0.2 11 | 12 | ``` 13 | 14 | ## 代码使用示例 15 | ```Java 16 | DocAutoGenerator docAutoGenerator = new DocAutoGenerator(); 17 | GlobalConfig globalConfig = new GlobalConfig(); 18 | globalConfig.setOutputDir("D:/doc/"); //输出目录 19 | globalConfig.setPackagePath("com.github.chuanzh.controller"); //controller包目录 20 | globalConfig.setInclude(new String[]{"UserController","AccountController"}); // 为空生成所有 21 | globalConfig.setOpen(true); //生成完文档后打开目录 22 | docAutoGenerator.setGlobalConfig(globalConfig); 23 | docAutoGenerator.execute(); 24 | ``` 25 | 26 | # 文档展示 27 | ![image](https://github.com/chuanzh/document-genteration/blob/master/doc/doc1.png) 28 | ![image](https://github.com/chuanzh/document-genteration/blob/master/doc/doc2.png) 29 | 30 | # 更多 31 | 1. 后续会支持HTML、word等更多格式 32 | 2. 支持请求、返回参数自定义过滤规则 33 | 3. 支持自定义模板 34 | 4. ... 35 | 36 | 欢迎使用并提供宝贵的意见,也欢迎你一起参与开发 37 | 联系邮箱:zhuangchuan0305@gmail.com 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/chuanzh/po/MethodInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.chuanzh.po; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * @author zhangchuan 7 | */ 8 | public class MethodInfo { 9 | 10 | /** 11 | * 请求地址 12 | */ 13 | private String requestUrl; 14 | 15 | /** 16 | * 请求方式 17 | */ 18 | private String requestType; 19 | 20 | /** 21 | * 请求方法描述 22 | */ 23 | private String desc; 24 | 25 | 26 | /** 27 | * 请求参数,如果请求参数都是基本类型,则封装在这个集合中 28 | */ 29 | private List requests; 30 | 31 | /** 32 | * 请求对象 33 | */ 34 | private String requestBeanName; 35 | 36 | /** 37 | * 返回对象 38 | */ 39 | private String responseBeanName; 40 | 41 | 42 | public String getRequestUrl() { 43 | return requestUrl; 44 | } 45 | 46 | public void setRequestUrl(String requestUrl) { 47 | this.requestUrl = requestUrl; 48 | } 49 | 50 | public String getRequestType() { 51 | return requestType; 52 | } 53 | 54 | public void setRequestType(String requestType) { 55 | this.requestType = requestType; 56 | } 57 | 58 | public String getDesc() { 59 | return desc; 60 | } 61 | 62 | public void setDesc(String desc) { 63 | this.desc = desc; 64 | } 65 | 66 | public String getRequestBeanName() { 67 | return requestBeanName; 68 | } 69 | 70 | public void setRequestBeanName(String requestBeanName) { 71 | this.requestBeanName = requestBeanName; 72 | } 73 | 74 | public String getResponseBeanName() { 75 | return responseBeanName; 76 | } 77 | 78 | public void setResponseBeanName(String responseBeanName) { 79 | this.responseBeanName = responseBeanName; 80 | } 81 | 82 | public List getRequests() { 83 | return requests; 84 | } 85 | 86 | public void setRequests(List requests) { 87 | this.requests = requests; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/github/chuanzh/engine/FreemarkerTemplateEngine.java: -------------------------------------------------------------------------------- 1 | package com.github.chuanzh.engine; 2 | 3 | import com.github.chuanzh.config.GlobalConfig; 4 | import com.github.chuanzh.config.TemplateConfig; 5 | import freemarker.template.Configuration; 6 | import freemarker.template.Template; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.io.File; 11 | import java.io.FileWriter; 12 | import java.io.IOException; 13 | import java.io.StringWriter; 14 | import java.util.Map; 15 | 16 | public class FreemarkerTemplateEngine extends AbstractTemplateEngine { 17 | 18 | private static final Logger logger = LoggerFactory.getLogger(FreemarkerTemplateEngine.class); 19 | 20 | private static Configuration cfg = new Configuration(); 21 | private Template temp = null; 22 | private static String encode = "UTF-8"; 23 | private GlobalConfig globalConfig; 24 | 25 | public FreemarkerTemplateEngine() { 26 | init(); 27 | } 28 | 29 | @Override 30 | public AbstractTemplateEngine init() { 31 | cfg.setClassForTemplateLoading(FreemarkerTemplateEngine.class,"/"); 32 | cfg.setDefaultEncoding(encode); 33 | try { 34 | this.temp = cfg.getTemplate(TemplateConfig.MARK_DOWN_TEMPLATE); 35 | } catch (IOException e) { 36 | logger.error("初始化模板失败", e); 37 | } 38 | return this; 39 | } 40 | 41 | @Override 42 | public AbstractTemplateEngine process(Map param) { 43 | StringWriter sw = new StringWriter(); 44 | try { 45 | this.temp.process(param, sw); 46 | } catch (Exception e) { 47 | logger.error("处理模板信息失败",e); 48 | } 49 | writeToFile(sw, param.get("path").toString()); 50 | return this; 51 | } 52 | 53 | private void writeToFile(StringWriter sw, String filePath) { 54 | FileWriter fw = null; 55 | try { 56 | File file = new File(filePath); 57 | if (!file.exists()) { 58 | file.createNewFile(); 59 | } 60 | fw = new FileWriter(file); 61 | fw.write(sw.toString()); 62 | } catch (IOException e) { 63 | logger.error("写入文件失败",e); 64 | } finally { 65 | try { 66 | fw.close(); 67 | } catch (IOException e) { 68 | logger.error("关闭文件流失败",e); 69 | } 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/github/chuanzh/config/GlobalConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.chuanzh.config; 2 | 3 | import com.github.chuanzh.enums.TemplateType; 4 | 5 | /** 6 | * @author zhangchuan 7 | * 全局配置 8 | */ 9 | public class GlobalConfig { 10 | 11 | /** 12 | * 请求参数过滤字段 13 | */ 14 | private String[] requestFilterFields; 15 | 16 | /** 17 | * 返回参数过滤字段 18 | */ 19 | private String[] responseFilterFields; 20 | 21 | /** 22 | * 输出路径 23 | */ 24 | private String outputDir; 25 | 26 | /** 27 | * control包名 28 | */ 29 | private String packagePath; 30 | 31 | /** 32 | * 执行完成后是否输出文件目录,默认不打开 33 | */ 34 | private boolean open = false; 35 | 36 | /** 37 | * 模板类型,默认为markdown 38 | */ 39 | private TemplateType templateType = TemplateType.MARKDOWN; 40 | 41 | /** 42 | * 是否忽略没有注解的字段,默认为否 43 | */ 44 | private boolean ignoreNoAnnotation = false; 45 | 46 | /** 47 | * 包含controller文件,为空表示所有 48 | */ 49 | private String[] include = new String[]{}; 50 | 51 | public String[] getRequestFilterFields() { 52 | return requestFilterFields; 53 | } 54 | 55 | public void setRequestFilterFields(String[] requestFilterFields) { 56 | this.requestFilterFields = requestFilterFields; 57 | } 58 | 59 | public String[] getResponseFilterFields() { 60 | return responseFilterFields; 61 | } 62 | 63 | public void setResponseFilterFields(String[] responseFilterFields) { 64 | this.responseFilterFields = responseFilterFields; 65 | } 66 | 67 | public String getOutputDir() { 68 | return outputDir; 69 | } 70 | 71 | public void setOutputDir(String outputDir) { 72 | this.outputDir = outputDir; 73 | } 74 | 75 | public String getPackagePath() { 76 | return packagePath; 77 | } 78 | 79 | public void setPackagePath(String packagePath) { 80 | this.packagePath = packagePath; 81 | } 82 | 83 | public boolean isOpen() { 84 | return open; 85 | } 86 | 87 | public void setOpen(boolean open) { 88 | this.open = open; 89 | } 90 | 91 | public TemplateType getTemplateType() { 92 | return templateType; 93 | } 94 | 95 | public void setTemplateType(TemplateType templateType) { 96 | this.templateType = templateType; 97 | } 98 | 99 | public boolean isIgnoreNoAnnotation() { 100 | return ignoreNoAnnotation; 101 | } 102 | 103 | public void setIgnoreNoAnnotation(boolean ignoreNoAnnotation) { 104 | this.ignoreNoAnnotation = ignoreNoAnnotation; 105 | } 106 | 107 | public String[] getInclude() { 108 | return include; 109 | } 110 | 111 | public void setInclude(String[] include) { 112 | this.include = include; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /templates/html.ftl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | model.html 5 | 6 | 7 | 8 | 9 | 10 | 11 | 53 | 63 | 64 | 65 | 66 |     

接口文档注释

67 | 68 | <#list interfaceTitles as interfaceTitle> 69 | * ${interfaceTitle} 70 | 71 | 72 | <#list interfaceDetails as interfaceDetail> 73 | 74 |

${interfaceDetail_index+1!}. ${interfaceDetail["title"]!}

75 |

${interfaceDetail["requestType"]!}

76 | 请求参数: 77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | <#list interfaceDetail["request"] as request> 86 | 87 | 88 | 89 | 96 | 97 | 98 | 99 |
字段名称类型是否必填说明
${request.name!}${request.type!} 90 | <#if request.isNotNull == 0> 91 | 否 92 | <#else> 93 | 是 94 | 95 | ${request.desc!}
100 | 101 |
102 | 103 | 响应参数: 104 | 105 | 106 | 107 | 108 | 109 | 110 | <#list interfaceDetail["response"] as response> 111 | 112 | 113 | 114 | 115 | 116 | 117 |
字段名称类型说明
${response.name!}${response.type!}${response.desc!}
118 |
119 | 回到顶部 120 |
121 |
122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/main/java/com/github/chuanzh/util/ClassHelperUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.chuanzh.util; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.lang.reflect.Field; 9 | import java.lang.reflect.Type; 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.HashSet; 13 | import java.util.List; 14 | 15 | /** 16 | * @author zhangchuan 17 | */ 18 | public class ClassHelperUtils { 19 | private static Logger logger = LoggerFactory.getLogger(ClassHelperUtils.class.getName()); 20 | 21 | private static String[] BASE_TYPE_ARR = {"boolean","char","byte","short","int","long","float","double","Boolean","Char","Byte","Short","Integer","Long","Float","Double","String","BigDecimal","Date"}; 22 | private static HashSet BASE_TYPE = new HashSet(); 23 | private static HashSet FILTER_FIELDS = new HashSet(); 24 | 25 | static { 26 | for (String s : BASE_TYPE_ARR) { 27 | BASE_TYPE.add(s); 28 | } 29 | FILTER_FIELDS.add("serialVersionUID"); 30 | } 31 | 32 | /** 33 | * 获取某个类及其父类的Field 34 | * @param clazz 类 35 | * @return 所有字段 36 | */ 37 | public static ArrayList findClassAllField (Class clazz){ 38 | ArrayList list = new ArrayList(); 39 | try { 40 | List clazzList = new ArrayList(); 41 | 42 | while(!"java.lang.Object".equals(clazz.getName())){ 43 | clazzList.add(clazz); 44 | clazz = clazz.getSuperclass(); 45 | if (clazz == null) { 46 | break; 47 | } 48 | } 49 | Collections.reverse(clazzList); 50 | for(int i=0;i")); 87 | } 88 | if ("java.util.List".equals(simpleName) && isBaseTypeName(subSimpleName)) { 89 | return true; 90 | } 91 | return false; 92 | } 93 | 94 | public static boolean isBaseTypeName(String simpleName) { 95 | if (BASE_TYPE.contains(simpleName)){ 96 | return true; 97 | } 98 | return false; 99 | } 100 | 101 | public static String getGenericTypeName(Field field) { 102 | String genericType = field.getGenericType().getTypeName(); 103 | if (genericType.indexOf("<") != -1) { 104 | String simpleType = field.getType().getSimpleName(); 105 | String simpleSubName = genericType.substring(genericType.lastIndexOf(".")+1); 106 | return simpleType +"<"+simpleSubName; 107 | } 108 | String typeName = genericType.substring(genericType.lastIndexOf(".")+1); 109 | if (typeName.equals("T")) { 110 | return "Object"; 111 | } 112 | return typeName; 113 | } 114 | 115 | public static String subClass(Type type) { 116 | String typeName = type.getTypeName(); 117 | String subTypeName = typeName; 118 | String simpleName = null; 119 | if (typeName.indexOf("<") != -1){ 120 | subTypeName = typeName.substring(typeName.lastIndexOf("<")+1, typeName.indexOf(">")); 121 | simpleName = subTypeName.substring(subTypeName.lastIndexOf(".")+1); 122 | } 123 | if (simpleName != null && BASE_TYPE.contains(simpleName)){ 124 | return null; 125 | } 126 | return subTypeName; 127 | } 128 | 129 | public static void mkdirs(String dirPath) { 130 | File dir = new File(dirPath); 131 | if (!dir.exists()) { 132 | boolean result = dir.mkdirs(); 133 | if (result) { 134 | logger.debug("创建目录: [" + dirPath + "]"); 135 | } 136 | } 137 | } 138 | 139 | public static void open(String dirPath) { 140 | try { 141 | String osName = System.getProperty("os.name"); 142 | if (osName != null) { 143 | if (osName.contains("Mac")) { 144 | Runtime.getRuntime().exec("open " + dirPath); 145 | } else if (osName.contains("Windows")) { 146 | Runtime.getRuntime().exec("cmd /c start " + dirPath); 147 | } else { 148 | logger.debug("文件输出目录:" + dirPath); 149 | } 150 | } 151 | } catch (IOException e) { 152 | logger.error("打开目录失败",e); 153 | } 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/com/github/chuanzh/util/ControllerInfoBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.chuanzh.util; 2 | 3 | 4 | import com.github.chuanzh.enums.RequestMode; 5 | import com.github.chuanzh.po.ControllerInfo; 6 | import com.github.chuanzh.po.MethodInfo; 7 | import com.github.chuanzh.po.Request; 8 | import io.swagger.annotations.Api; 9 | import io.swagger.annotations.ApiOperation; 10 | import io.swagger.annotations.ApiParam; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | import org.springframework.web.bind.annotation.PostMapping; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | 17 | import java.io.File; 18 | import java.lang.reflect.Method; 19 | import java.lang.reflect.Parameter; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | /** 24 | * @author zhangchuan 25 | */ 26 | public class ControllerInfoBuilder { 27 | 28 | private static Logger logger = LoggerFactory.getLogger(ControllerInfoBuilder.class); 29 | 30 | public List handle(String packageName, List includes){ 31 | List resultList = new ArrayList(); 32 | try { 33 | List classNames = getClassName(packageName); 34 | for (String className : classNames) { 35 | if (includes.size() >0 && !includes.contains(className.substring(className.lastIndexOf(".")+1))) { 36 | continue; 37 | } 38 | ControllerInfo controllerInfo = new ControllerInfo(); 39 | Class clazz = Class.forName(className); 40 | if (clazz.getAnnotation(Api.class) != null) { 41 | controllerInfo.setDesc(((Api) clazz.getAnnotation(Api.class)).tags()[0]); 42 | } else { 43 | controllerInfo.setDesc(clazz.getSimpleName()); 44 | } 45 | controllerInfo.setMethodInfoList(getMethodInfoByClass(clazz)); 46 | resultList.add(controllerInfo); 47 | } 48 | } catch (Exception e) { 49 | logger.error("获取control类失败",e); 50 | } 51 | return resultList; 52 | } 53 | 54 | private List getClassName(String packageName) { 55 | List classNames = null; 56 | String filePath = ClassLoader.getSystemResource("").getPath() + packageName.replace(".", "/"); 57 | classNames = getClassName(filePath, null); 58 | return classNames; 59 | } 60 | 61 | private List getClassName(String filePath, List classNameList) { 62 | filePath = filePath.replace("\\", "/"); 63 | List myClassName = new ArrayList(); 64 | File file = new File(filePath); 65 | File[] childFiles = file.listFiles(); 66 | for (File childFile : childFiles) { 67 | if (childFile.isDirectory()) { 68 | myClassName.addAll(getClassName(childFile.getPath(), myClassName)); 69 | } else { 70 | String childFilePath = childFile.getPath(); 71 | if (!childFilePath.startsWith("/")) { 72 | childFilePath = childFilePath.substring(childFilePath.indexOf("\\classes") + 9, childFilePath.lastIndexOf(".")); 73 | } else { 74 | childFilePath = childFilePath.substring(childFilePath.indexOf("/classes") + 9, childFilePath.lastIndexOf(".")); 75 | } 76 | childFilePath = childFilePath.replace("\\", ".").replace("/", "."); 77 | String lastString = childFilePath.substring(childFilePath.length()-1, childFilePath.length()); 78 | if(childFilePath.indexOf(".svn")<0&&!".".equals(lastString)){ 79 | myClassName.add(childFilePath); 80 | } 81 | } 82 | } 83 | 84 | return myClassName; 85 | } 86 | 87 | private List getMethodInfoByClass(Class clazz) { 88 | String classRequestMapping = ""; 89 | if(clazz.getAnnotation(RequestMapping.class)!=null){ 90 | classRequestMapping = ((RequestMapping)clazz.getAnnotation(RequestMapping.class)).value()[0]; 91 | } 92 | List methodInfoList = new ArrayList<>(); 93 | Method[] methods = clazz.getDeclaredMethods(); 94 | for (Method method : methods) { 95 | MethodInfo methodInfo = new MethodInfo(); 96 | methodInfo.setRequestUrl(classRequestMapping+getRequestUrl(method)); 97 | methodInfo.setRequestType(getRequestType(method)); 98 | 99 | if (method.getAnnotation(ApiOperation.class) != null) { 100 | methodInfo.setDesc(((ApiOperation)method.getAnnotation(ApiOperation.class)).value()); 101 | } 102 | 103 | Parameter[] parameters = method.getParameters(); 104 | List requests = new ArrayList<>(); 105 | Request request = null; 106 | for (Parameter parameter : parameters) { 107 | if (ClassHelperUtils.isBaseType(parameter.getParameterizedType()) 108 | || ClassHelperUtils.isListBaseType(parameter.getParameterizedType())) { 109 | request = new Request(); 110 | request.setName(parameter.getName()); 111 | request.setType(parameter.getType().getSimpleName()); 112 | request.setIsNotNull(1); 113 | if (parameter.getAnnotation(ApiParam.class) !=null) { 114 | ApiParam apiParam = parameter.getAnnotation(ApiParam.class); 115 | request.setDesc(apiParam.value()); 116 | } 117 | requests.add(request); 118 | } else { 119 | methodInfo.setRequestBeanName(parameter.getParameterizedType().getTypeName()); 120 | } 121 | } 122 | if (requests.size()>0) { 123 | methodInfo.setRequests(requests); 124 | } 125 | methodInfo.setResponseBeanName(method.getGenericReturnType().getTypeName()); 126 | methodInfoList.add(methodInfo); 127 | } 128 | return methodInfoList; 129 | } 130 | 131 | private String getRequestUrl(Method method) { 132 | if (method.getAnnotation(PostMapping.class) != null) { 133 | return ((PostMapping) method.getAnnotation(PostMapping.class)).value()[0]; 134 | } 135 | 136 | if (method.getAnnotation(GetMapping.class) != null) { 137 | return ((GetMapping) method.getAnnotation(GetMapping.class)).value()[0]; 138 | } 139 | 140 | if (method.getAnnotation(RequestMapping.class) != null) { 141 | return ((RequestMapping) method.getAnnotation(RequestMapping.class)).value()[0]; 142 | } 143 | 144 | return ""; 145 | } 146 | 147 | private String getRequestType(Method method) { 148 | if (method.getAnnotation(PostMapping.class) != null) { 149 | return RequestMode.POST.name(); 150 | } 151 | 152 | if (method.getAnnotation(GetMapping.class) != null) { 153 | return RequestMode.GET.name(); 154 | } 155 | 156 | if (method.getAnnotation(RequestMapping.class) != null) { 157 | return RequestMode.GET_OR_POST.name(); 158 | } 159 | 160 | return ""; 161 | } 162 | 163 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.github.chuanzh 7 | document-generation 8 | 1.0.1 9 | jar 10 | 11 | document-generation 12 | 接口文档自动生成 13 | https://github.com/chuanzh/document-genteration 14 | 15 | 16 | 17 | The Apache Software License, Version 2.0 18 | http://www.apache.org/licenses/LICENSE-2.0.txt 19 | 20 | 21 | 22 | 23 | 24 | chuan.zhang 25 | zhangchuan0305@gmail.com 26 | 27 | 28 | 29 | 30 | scm:git@github.com:chuanzh/document-generation.git 31 | scm:git@github.com:chuanzh/document-generation.git 32 | git@github.com:chuanzh/document-generation.git 33 | 34 | 35 | 36 | 1.8 37 | UTF-8 38 | UTF-8 39 | 40 | 41 | 42 | 43 | org.springframework 44 | spring-web 45 | 5.0.7.RELEASE 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-log4j2 51 | 2.0.3.RELEASE 52 | 53 | 54 | 55 | org.freemarker 56 | freemarker 57 | 2.3.28 58 | 59 | 60 | 61 | io.swagger 62 | swagger-annotations 63 | 1.5.20 64 | 65 | 66 | org.springframework 67 | spring-context-support 68 | 5.0.7.RELEASE 69 | 70 | 71 | 72 | 73 | 74 | release 75 | 76 | 77 | oss 78 | https://oss.sonatype.org/content/repositories/snapshots/ 79 | 80 | 81 | oss 82 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 83 | 84 | 85 | 86 | 87 | 88 | ${project.basedir} 89 | 90 | templates/*.ftl 91 | 92 | 93 | 94 | 95 | 96 | org.apache.maven.plugins 97 | maven-compiler-plugin 98 | 3.8.1 99 | 100 | 1.8 101 | 1.8 102 | utf-8 103 | 104 | 105 | 106 | 107 | org.apache.maven.plugins 108 | maven-source-plugin 109 | 3.0.1 110 | 111 | 112 | package 113 | 114 | jar-no-fork 115 | 116 | 117 | 118 | 119 | 120 | 121 | org.apache.maven.plugins 122 | maven-javadoc-plugin 123 | 2.10.4 124 | 125 | 126 | package 127 | 128 | jar 129 | 130 | 131 | 132 | 133 | 134 | 135 | org.apache.maven.plugins 136 | maven-gpg-plugin 137 | 1.6 138 | 139 | 140 | sign-artifacts 141 | verify 142 | 143 | sign 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /src/main/java/com/github/chuanzh/DocAutoGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.chuanzh; 2 | 3 | import com.github.chuanzh.config.GlobalConfig; 4 | import com.github.chuanzh.engine.AbstractTemplateEngine; 5 | import com.github.chuanzh.engine.FreemarkerTemplateEngine; 6 | import com.github.chuanzh.po.ControllerInfo; 7 | import com.github.chuanzh.po.MethodInfo; 8 | import com.github.chuanzh.po.Request; 9 | import com.github.chuanzh.po.Response; 10 | import com.github.chuanzh.util.ClassHelperUtils; 11 | import com.github.chuanzh.util.ControllerInfoBuilder; 12 | import io.swagger.annotations.ApiModelProperty; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.io.IOException; 17 | import java.lang.reflect.Field; 18 | import java.util.*; 19 | 20 | /** 21 | * @author zhangchuan 22 | */ 23 | public class DocAutoGenerator { 24 | 25 | private Logger logger = LoggerFactory.getLogger(DocAutoGenerator.class); 26 | 27 | private ControllerInfoBuilder controllerInfoBuilder = new ControllerInfoBuilder(); 28 | private GlobalConfig globalConfig; 29 | private AbstractTemplateEngine abstractTemplateEngine; 30 | 31 | public void execute() { 32 | if (!checkConfig()) { 33 | return; 34 | } 35 | List> params = null; 36 | try { 37 | params = buildTemplateParam(globalConfig.getOutputDir(), globalConfig.getPackagePath(), Arrays.asList(globalConfig.getInclude())); 38 | } catch (IOException e) { 39 | logger.info("构建模板参数失败", e); 40 | return; 41 | } 42 | 43 | if (null == abstractTemplateEngine) { 44 | abstractTemplateEngine = new FreemarkerTemplateEngine(); 45 | } 46 | ClassHelperUtils.mkdirs(globalConfig.getOutputDir()); 47 | for (Map param : params) { 48 | abstractTemplateEngine.process(param); 49 | logger.info("生成接口文档完成:"+param.get("path")); 50 | } 51 | if (globalConfig.isOpen()){ 52 | ClassHelperUtils.open(globalConfig.getOutputDir()); 53 | } 54 | } 55 | 56 | private boolean checkConfig() { 57 | if (null == globalConfig) { 58 | logger.info("请配置globalConfig参数"); 59 | return false; 60 | } 61 | if (globalConfig.getOutputDir() == null) { 62 | logger.info("请配置输出目录"); 63 | return false; 64 | 65 | } 66 | if (globalConfig.getPackagePath() == null) { 67 | logger.info("请配置Controller包目录"); 68 | return false; 69 | } 70 | return true; 71 | } 72 | 73 | private List> buildTemplateParam(String docPath, String packageName, List includes) throws IOException { 74 | List> params = new ArrayList<>(); 75 | Map param = null; 76 | List controllerInfoList = controllerInfoBuilder.handle(packageName, includes); 77 | for (ControllerInfo controllerInfo : controllerInfoList) { 78 | List interfaceTitles = new ArrayList(); 79 | List> interfaceDetails = new ArrayList>(); 80 | HashMap interfaceDetail = null; 81 | 82 | for (MethodInfo methodInfo : controllerInfo.getMethodInfoList()) { 83 | interfaceTitles.add(methodInfo.getDesc()); 84 | interfaceDetail = new HashMap(); 85 | interfaceDetail.put("title", methodInfo.getDesc()); 86 | interfaceDetail.put("requestType", String.format("(%s)%s",methodInfo.getRequestType(), methodInfo.getRequestUrl())); 87 | interfaceDetail.put("request", buildRequestInfo(methodInfo)); 88 | interfaceDetail.put("response", buildResponseInfo(methodInfo)); 89 | interfaceDetails.add(interfaceDetail); 90 | } 91 | 92 | param = new HashMap<>(); 93 | param.put("path", docPath+controllerInfo.getDesc()+".md"); 94 | param.put("title", controllerInfo.getDesc()); 95 | param.put("interfaceTitles", interfaceTitles); 96 | param.put("interfaceDetails", interfaceDetails); 97 | params.add(param); 98 | } 99 | return params; 100 | } 101 | 102 | private LinkedHashMap> buildRequestInfo(MethodInfo methodInfo) { 103 | LinkedHashMap> requestList = new LinkedHashMap<>(); 104 | Request request = null; 105 | try { 106 | if (methodInfo.getRequests() != null) { 107 | requestList.put("",methodInfo.getRequests()); 108 | } 109 | if (methodInfo.getRequestBeanName() != null) { 110 | String requestBeanName = methodInfo.getRequestBeanName(); 111 | String subRequestBeanName = null; 112 | if (requestBeanName.indexOf("<") != -1) { 113 | requestBeanName = requestBeanName.substring(0, requestBeanName.indexOf("<")); 114 | subRequestBeanName = methodInfo.getRequestBeanName().substring(methodInfo.getRequestBeanName().lastIndexOf("<")+1, methodInfo.getRequestBeanName().indexOf(">")); 115 | } 116 | if (!ClassHelperUtils.isListTypeName(requestBeanName)) { 117 | doRequestInfo(requestBeanName, requestList); 118 | } 119 | if (subRequestBeanName != null 120 | && !ClassHelperUtils.isBaseTypeName(subRequestBeanName.substring(subRequestBeanName.lastIndexOf(".")+1))) { 121 | doRequestInfo(subRequestBeanName, requestList); 122 | } 123 | } 124 | } catch (Exception e) { 125 | logger.error("构建请求参数失败",e); 126 | } 127 | return requestList; 128 | } 129 | 130 | private void doRequestInfo(String beanName, LinkedHashMap> requestList) { 131 | Class requestClass = null; 132 | try { 133 | requestClass = Class.forName(beanName); 134 | } catch (ClassNotFoundException e) { 135 | return; 136 | } 137 | List requestFields = ClassHelperUtils.findClassAllField(requestClass); 138 | List requests = new ArrayList<>(); 139 | if (requestList.size() == 0) { 140 | requestList.put("first",requests); 141 | } else { 142 | requestList.put(requestClass.getSimpleName(),requests); 143 | } 144 | for (Field field : requestFields) { 145 | if (ClassHelperUtils.isFilterField(field.getName())) { 146 | continue; 147 | } 148 | Request request = new Request(); 149 | request.setName(field.getName()); 150 | request.setType(ClassHelperUtils.getGenericTypeName(field)); 151 | if (field.getAnnotation(ApiModelProperty.class) !=null) { 152 | ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class); 153 | request.setDesc(apiModelProperty.value()); 154 | request.setIsNotNull(apiModelProperty.required()?1:0); 155 | } 156 | requests.add(request); 157 | if (!ClassHelperUtils.isBaseType(field.getGenericType())){ 158 | String subClass = ClassHelperUtils.subClass(field.getGenericType()); 159 | /** 子类不能等于父类,避免死循环 */ 160 | if (subClass != null && !subClass.equals(beanName)) { 161 | doRequestInfo(subClass, requestList); 162 | } 163 | } 164 | } 165 | } 166 | 167 | private void doResponseInfo(String beanName, LinkedHashMap> responseList) { 168 | Class responseClass = null; 169 | try { 170 | responseClass = Class.forName(beanName); 171 | } catch (ClassNotFoundException e) { 172 | return; 173 | } 174 | List responseFields = ClassHelperUtils.findClassAllField(responseClass); 175 | List responses = new ArrayList<>(); 176 | if (responseList.size() == 0) { 177 | responseList.put("first",responses); 178 | } else { 179 | responseList.put(responseClass.getSimpleName(),responses); 180 | } 181 | for (Field field : responseFields) { 182 | if (ClassHelperUtils.isFilterField(field.getName())) { 183 | continue; 184 | } 185 | Response response = new Response(); 186 | response.setName(field.getName()); 187 | response.setType(ClassHelperUtils.getGenericTypeName(field)); 188 | if (field.getAnnotation(ApiModelProperty.class) !=null) { 189 | ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class); 190 | response.setDesc(apiModelProperty.value()); 191 | } 192 | responses.add(response); 193 | if (!ClassHelperUtils.isBaseType(field.getGenericType())){ 194 | String subClass = ClassHelperUtils.subClass(field.getGenericType()); 195 | /** 子类不能等于父类,避免死循环 */ 196 | if (subClass != null && !subClass.equals(beanName)) { 197 | doResponseInfo(subClass, responseList); 198 | } 199 | } 200 | } 201 | } 202 | 203 | private LinkedHashMap> buildResponseInfo(MethodInfo methodInfo) { 204 | LinkedHashMap> responseList = new LinkedHashMap<>(); 205 | try { 206 | String responseBeanName = methodInfo.getResponseBeanName(); 207 | String subResponseBeanName = null; 208 | if (responseBeanName.indexOf("<") != -1) { 209 | responseBeanName = responseBeanName.substring(0, responseBeanName.indexOf("<")); 210 | subResponseBeanName = methodInfo.getResponseBeanName().substring(methodInfo.getResponseBeanName().lastIndexOf("<")+1, methodInfo.getResponseBeanName().indexOf(">")); 211 | } 212 | 213 | if (!ClassHelperUtils.isListTypeName(responseBeanName)) { 214 | doResponseInfo(responseBeanName, responseList); 215 | } 216 | if (subResponseBeanName != null 217 | && !ClassHelperUtils.isBaseTypeName(subResponseBeanName.substring(subResponseBeanName.lastIndexOf(".")+1))) { 218 | doResponseInfo(subResponseBeanName, responseList); 219 | } 220 | 221 | } catch (Exception e) { 222 | logger.error("构建请求参数失败",e); 223 | } 224 | return responseList; 225 | } 226 | 227 | 228 | public GlobalConfig getGlobalConfig() { 229 | return globalConfig; 230 | } 231 | 232 | public void setGlobalConfig(GlobalConfig globalConfig) { 233 | this.globalConfig = globalConfig; 234 | } 235 | } 236 | --------------------------------------------------------------------------------