maven+spring-boot+springfox+swagger2markup+spring restdoc+asciidoctor生成完美的rest文档

http://blog.csdn.net/daisy_xiu/article/details/52368920

写了一个工程,要写文档,相信所有的程序员都和我一样,最讨厌写文档,有没有片动生成文档的东西呢?有!

首先,我们要引入swagger。

1、swagger

什么是swagger?说白了,就是可以帮你生成一个可以测试接口的页面的工具。具体在这里:http://swagger.io/open-source-integrations/。多得我也不说了,文档很多,具体可以看这里:http://blog.sina.com.cn/s/blog_72ef7bea0102vpu7.html。说这个东西的的原因是,springfox是依赖这东西的。


2、springfox

为什么说springfox是依赖swagger的呢?因为swagger本身不支持spring mvc的,springfox把swagger包装了一下,让他可以支持springmvc。

我的项目是用spring-boot做的,基础知识就不在这里说了。只说怎么玩。

先是maven的引入:

[html]  view plain  copy
 
  1. <dependency>  
  2.     <groupId>io.springfoxgroupId>  
  3.     <artifactId>springfox-swagger-uiartifactId>  
  4.     <version>2.5.0version>  
  5. dependency>  
  6. <dependency>  
  7.     <groupId>io.springfoxgroupId>  
  8.     <artifactId>springfox-swagger2artifactId>  
  9.     <version>2.5.0version>  
  10. dependency>  
  11. <dependency>  
  12.     <groupId>org.springframework.restdocsgroupId>  
  13.     <artifactId>spring-restdocs-mockmvcartifactId>  
  14.     <version>1.1.1.RELEASEversion>  
  15. dependency>  
  16. <dependency>  
  17.     <groupId>io.springfoxgroupId>  
  18.     <artifactId>springfox-staticdocsartifactId>  
  19.     <version>2.5.0version>  
  20.     <scope>testscope>  
  21. dependency>  


我先写一个config类,看不懂的自己补下spring-boot:

[java]  view plain  copy
 
  1. package doc.base;  
  2.   
  3. import lombok.extern.log4j.Log4j2;  
  4. import org.springframework.boot.bind.RelaxedPropertyResolver;  
  5. import org.springframework.context.EnvironmentAware;  
  6. import org.springframework.context.annotation.Bean;  
  7. import org.springframework.context.annotation.ComponentScan;  
  8. import org.springframework.context.annotation.Configuration;  
  9. import org.springframework.core.env.Environment;  
  10. import org.springframework.util.StopWatch;  
  11. import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;  
  12. import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;  
  13. import springfox.documentation.builders.RequestHandlerSelectors;  
  14. import springfox.documentation.service.ApiInfo;  
  15. import springfox.documentation.service.Contact;  
  16. import springfox.documentation.spi.DocumentationType;  
  17. import springfox.documentation.spring.web.plugins.Docket;  
  18. import springfox.documentation.swagger2.annotations.EnableSwagger2;  
  19.   
  20. import static springfox.documentation.builders.PathSelectors.*;  
  21. import static com.google.common.base.Predicates.*;  
  22.   
  23. @Configuration  
  24. @EnableSwagger2//注意这里  
  25. @ComponentScan(basePackages = "doc")  
  26. @Log4j2  
  27. public class SwaggerConfig extends WebMvcConfigurerAdapter  
  28.         implements EnvironmentAware {  
  29.   
  30.     /** 
  31.      * 静态资源映射 
  32.      *  
  33.      * @param registry 
  34.      *            静态资源注册器 
  35.      */  
  36.     public void addResourceHandlers(ResourceHandlerRegistry registry) {  
  37.         registry.addResourceHandler("swagger-ui.html")  
  38.                 .addResourceLocations("classpath:/META-INF/resources/");  
  39.   
  40.         registry.addResourceHandler("/webjars/**")  
  41.                 .addResourceLocations("classpath:/META-INF/resources/webjars/");  
  42.         super.addResourceHandlers(registry);  
  43.     }  
  44.   
  45.     @Override  
  46.     public void setEnvironment(Environment environment) {//这里是从配置文件里读相关的字段  
  47.         this.propertyResolver = new RelaxedPropertyResolver(environment,  
  48.                 "swagger.");  
  49.     }  
  50.   
  51.     @Bean  
  52.     public Docket swaggerSpringfoxDocket4KAD() {//最重要的就是这里,定义了/test/.*开头的rest接口都分在了test分组里,可以通过/v2/api-docs?group=test得到定义的json  
  53.         log.debug("Starting Swagger");  
  54.         StopWatch watch = new StopWatch();  
  55.         watch.start();  
  56.         Docket swaggerSpringMvcPlugin = new Docket(DocumentationType.SWAGGER_2)  
  57.                 .groupName("test")  
  58.                 .apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.any())  
  59.                 .paths(regex("/test/.*")) // and by paths  
  60.                 .build();  
  61.         watch.stop();  
  62.         log.debug("Started Swagger in {} ms", watch.getTotalTimeMillis());  
  63.         return swaggerSpringMvcPlugin;  
  64.     }  
  65.   
  66.     private ApiInfo apiInfo() {//这里是生成文档基本信息的地方  
  67.         return new ApiInfo(propertyResolver.getProperty("title"),  
  68.                 propertyResolver.getProperty("description"),  
  69.                 propertyResolver.getProperty("version"),  
  70.                 propertyResolver.getProperty("termsOfServiceUrl"),  
  71.                 new Contact(propertyResolver.getProperty("contact.name"),  
  72.                         propertyResolver.getProperty("contact.url"),  
  73.                         propertyResolver.getProperty("contact.email")),  
  74.                 propertyResolver.getProperty("license"),  
  75.                 propertyResolver.getProperty("licenseUrl"));  
  76.     }  
  77.   
  78.     private RelaxedPropertyResolver propertyResolver;  
  79. }  
由于spring-mvc代理了/*,所以要把swagger-ui.html和/webjars/**做为静态资源放出来,不然无法访问。

然后,我们就可以在类上面加上swagger的注解了,只有这样,swagger才能生成文档:

[java]  view plain  copy
 
  1. @ApiOperation(  
  2.             value = "get",  
  3.             httpMethod = "GET",  
  4.             response = String.class,  
  5.             notes = "调用test get",  
  6.             produces = MediaType.APPLICATION_JSON_VALUE)//这是接口的基本信息,不解释,自己看吧  
  7.     @Snippet(  
  8.             url = "/test/get",  
  9.             snippetClass = MonitorControllerSnippet.Get.class)//这是我自己写的,方便spring-restdoc使用的,后面就说  
  10.     @ApiImplicitParams({//这个是入参,因为入参是request,所以要在这里定义,如果是其它的比如spring或javabean入参,可以在参数上使用@ApiParam注解  
  11.             @ApiImplicitParam(  
  12.                     name = "Service",  
  13.                     value = "服务",  
  14.                     required = true,  
  15.                     defaultValue = "monitor",  
  16.                     dataType = "String"),  
  17.             @ApiImplicitParam(  
  18.                     name = "Region",  
  19.                     value = "机房",  
  20.                     required = true,  
  21.                     dataType = "String"),  
  22.             @ApiImplicitParam(  
  23.                     name = "Version",  
  24.                     value = "版本",  
  25.                     required = true,  
  26.                     dataType = "String"),  
  27.             @ApiImplicitParam(  
  28.                     name = "name",  
  29.                     value = "名称",  
  30.                     example = "kaddefault",  
  31.                     required = true,  
  32.                     dataType = "String"),  
  33.             @ApiImplicitParam(  
  34.                     name = "producttype",  
  35.                     value = "产品类型",  
  36.                     example = "12",  
  37.                     required = true,  
  38.                     dataType = "int"),  
  39.             @ApiImplicitParam(  
  40.                     name = "tags",  
  41.                     dataType = "String",  
  42.                     example = "{\"port\":8080}")  
  43.     })  
  44.     @RequestMapping(  
  45.             path = "/test/get",  
  46.             method = RequestMethod.GET)  
  47.     public String get(HttpServletRequest request) {  
  48.         log.debug("进入get");  
  49.         return call4form(request);  
  50.     }  

然后我们调用一下http://localhost:8080/swagger-ui.html就可以看到了。

maven+spring-boot+springfox+swagger2markup+spring restdoc+asciidoctor生成完美的rest文档_第1张图片

好了,我们现在可以用swagger-ui调试spring-mvc了,这只是第一步。

下面,我们要使用springfox生成文档。这里要使用swagger2markup来进行转换。


3、spring restdoc

spring restdoc就是生成例子用的。先用它把每一个接口都调用一遍,会生成一堆acsiidoc文件。但是如果一个一个调,就把代码写死了,于是我写了一个自定的注解去完成这个工作:

[java]  view plain  copy
 
  1. @Target(ElementType.METHOD)  
  2. @Retention(RetentionPolicy.RUNTIME)  
  3. public @interface Snippet {  
  4.   
  5.     String httpMethod() default "GET";  
  6.   
  7.     String url() default "/";  
  8.   
  9.     String mediaType() default "application/x-www-form-urlencoded";  
  10.   
  11.     // 用于生成片断的类,需要是test.doc.swagger.snippet.Snippet类的实现  
  12.     Class snippetClass();  
  13. }  

接口:

[java]  view plain  copy
 
  1. /** 
  2.  * 

     

  3.  * 这是生成片断的方法所必须实现的接口 
  4.  * 

     
  5.  * Created by MiaoJia([email protected]) on 2016/8/26. 
  6.  */  
  7. public interface ISnippet {  
  8.     /** 
  9.      * 插入httpMethod 
  10.      *  
  11.      * @param httpMethod 
  12.      *            GET or POST 
  13.      */  
  14.     void setHttpMethod(String httpMethod);  
  15.   
  16.     /** 
  17.      * 获取Http Method 
  18.      * 
  19.      * @return Http Method 
  20.      */  
  21.     String getHttpMethod();  
  22.   
  23.     /** 
  24.      * 插入mediaType 
  25.      *  
  26.      * @param mediaType 
  27.      *            application/x-www-form-urlencoded or application/json 
  28.      */  
  29.     void setMediaType(String mediaType);  
  30.   
  31.     /** 
  32.      * 获取MediaType 
  33.      *  
  34.      * @return MediaType 
  35.      */  
  36.     MediaType getMediaType() ;  
  37.   
  38.     /** 
  39.      * 插入URL 
  40.      * 
  41.      * @param url 
  42.      *            URL 
  43.      */  
  44.     void setURL(String url);  
  45.   
  46.     /** 
  47.      * 获取URL 
  48.      *  
  49.      * @return url 
  50.      */  
  51.     String getURL();  
  52.   
  53.     /** 
  54.      * 获取入参JSONs 
  55.      * 
  56.      * @return Json 
  57.      */  
  58.     String getContent();  
  59.   
  60.     /** 
  61.      * 获取入参 
  62.      *  
  63.      * @return MultiValueMap 
  64.      */  
  65.     MultiValueMap getParams();  
  66.   
  67.     /** 
  68.      * 得到头 
  69.      *  
  70.      * @return HttpHeaders 
  71.      */  
  72.     HttpHeaders getHeaders();  
  73.   
  74.     /** 
  75.      * 得到头Cookie 
  76.      * @return Cookie 
  77.      */  
  78.     Cookie[] getCookie();  
  79.   
  80. }  

抽象实现类:

[java]  view plain  copy
 
  1. public abstract class ASnippet implements ISnippet {  
  2.   
  3.     @Override  
  4.     public void setHttpMethod(String httpMethod) {  
  5.         this.httpMethod = httpMethod;  
  6.     }  
  7.   
  8.     @Override  
  9.     public String getHttpMethod() {  
  10.         return httpMethod;  
  11.     }  
  12.   
  13.     @Override  
  14.     public void setMediaType(String mediaType) {  
  15.         this.mediaType = MediaType.valueOf(mediaType);  
  16.     }  
  17.   
  18.     @Override  
  19.     public MediaType getMediaType() {  
  20.         return mediaType;  
  21.     }  
  22.   
  23.     @Override  
  24.     public void setURL(String url) {  
  25.         this.url = url;  
  26.     }  
  27.   
  28.     @Override  
  29.     public String getURL() {  
  30.         return url;  
  31.     }  
  32.   
  33.     @Override  
  34.     public HttpHeaders getHeaders() {  
  35.         return new HttpHeaders();  
  36.     }  
  37.   
  38.     @Override  
  39.     public String getContent() {  
  40.         return null;  
  41.     }  
  42.   
  43.     @Override  
  44.     public Cookie[] getCookie() {  
  45.         return new Cookie[0];  
  46.     }  
  47.   
  48.     String httpMethod;  
  49.     MediaType mediaType;  
  50.     String url;  
  51.   
  52. }  

对应的实现类:

[html]  view plain  copy
 
  1. public class MonitorControllerSnippet {  
  2.   
  3.     /**  
  4.      * 抽象类  
  5.      */  
  6.     abstract static class BaseMonitorControllerSnippet extends ASnippet {  
  7.   
  8.         public MultiValueMap<String, String> getParams() {  
  9.             MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();  
  10.             parameters.put("Version", Collections.singletonList("2016-07-26"));  
  11.             parameters.put("Region",  
  12.                     Collections.singletonList("cn-shanghai-3"));  
  13.             parameters.put("Service", Collections.singletonList("monitor"));  
  14.   
  15.             return parameters;  
  16.         }  
  17.   
  18.         @Override  
  19.         public Cookie[] getCookie() {  
  20.             Cookie cookie = new Cookie(PassportAPI.USER_TOKEN_KSCDIGEST,  
  21.                     "046011086e3e617b98b7a6aa4cae88fc-668349870");  
  22.             return new Cookie[] {  
  23.                     cookie  
  24.             };  
  25.         }  
  26.     }  
  27.   
  28.     /**  
  29.      * get方法的  
  30.      */  
  31.     public static class Get extends BaseMonitorControllerSnippet {  
  32.         public MultiValueMap<String, String> getParams() {  
  33.             MultiValueMap<String, String> parameters = super.getParams();  
  34.             parameters.put("name", Collections.singletonList("kaddefault"));  
  35.             parameters.put("instance", Collections  
  36.                     .singletonList("0faae51b-e91f-4583-b83e-6b696d03d6b1"));  
  37.             parameters.put("producttype", Collections.singletonList("12"));  
  38.   
  39.             return parameters;  
  40.         }  
  41.   
  42.     }  
  43. }  


有的注解类,还要有一个读注解的类:

[java]  view plain  copy
 
  1. @Component  
  2. @Log4j2  
  3. public class ScanSnippet {  
  4.   
  5.     /** 
  6.      * 查询所有的拥有@ApiOperation注解和@Snippet注解的方法,找到@Snippet注解中定义的snippetClass,放入缓存备用 
  7.      * 
  8.      * @param basePackages 扫描路径 
  9.      * @return 扫描到的类 
  10.      */  
  11.     private void doScan(String basePackages) throws Exception {  
  12.         ScanUtils.scanner(basePackages, classMetadata -> {  
  13.             Class beanClass = this.getClass().getClassLoader()  
  14.                     .loadClass(classMetadata.getClassName());  
  15.             for (Method method : beanClass.getMethods()) {  
  16.                 ApiOperation apiOperation = method  
  17.                         .getAnnotation(ApiOperation.class);  
  18.                 Snippet snippet = method.getAnnotation(Snippet.class);  
  19.                 if (apiOperation != null && snippet != null) {  
  20.                     String apiName = apiOperation.value();  
  21.                     Class snippetClass = snippet.snippetClass();  
  22.                     if (ISnippet.class.isAssignableFrom(snippetClass)) {  
  23.                         try {  
  24.                             ISnippet _snippet = (ISnippet) snippetClass  
  25.                                     .newInstance();  
  26.                             _snippet.setHttpMethod(snippet.httpMethod());  
  27.                             _snippet.setMediaType(snippet.mediaType());  
  28.                             _snippet.setURL(snippet.url());  
  29.   
  30.                             log.info("扫描到了:apiName={},_snippet={}", apiName,  
  31.                                     _snippet);  
  32.                             snippetMap.put(apiName, _snippet);  
  33.                         } catch (InstantiationException  
  34.                                 | IllegalAccessException e) {  
  35.                             e.printStackTrace();  
  36.                         }  
  37.                     }  
  38.                 }  
  39.             }  
  40.         });  
  41.     }  
  42.   
  43.     /** 
  44.      * 启动时扫描 
  45.      */  
  46.     @PostConstruct  
  47.     public void scanSnippetMethod() {  
  48.         try {  
  49.             this.doScan("test");  
  50.         } catch (Exception e) {  
  51.             e.printStackTrace();  
  52.         }  
  53.     }  
  54.   
  55.     /** 
  56.      * snippetMap 
  57.      */  
  58.     public final static Map snippetMap = new HashMap<>();  
这里用了扫描:
[java]  view plain  copy
 
  1. package test.util.classreading;  
  2.   
  3. import java.io.File;  
  4. import java.io.FileFilter;  
  5. import java.io.FileInputStream;  
  6. import java.io.IOException;  
  7. import java.io.InputStream;  
  8. import java.net.JarURLConnection;  
  9. import java.net.URL;  
  10. import java.net.URLDecoder;  
  11. import java.util.Enumeration;  
  12. import java.util.LinkedHashSet;  
  13. import java.util.Set;  
  14. import java.util.jar.JarEntry;  
  15. import java.util.jar.JarFile;  
  16.   
  17. import org.objectweb.asm.ClassReader;  
  18. import org.objectweb.asm.Opcodes;  
  19.   
  20. public class ScanUtils {  
  21.   
  22.     /** 
  23.      * 从包package中获取所有的Class 
  24.      *  
  25.      * @return 
  26.      * @throws Exception 
  27.      */  
  28.     public static Set scanner(  
  29.             String resourcePath,  
  30.             ScannerHandle scannerHandle) throws Exception {  
  31.   
  32.         // 第一个class类的集合  
  33.         Set classes = new LinkedHashSet();  
  34.         // 是否循环迭代  
  35.         boolean recursive = true;  
  36.         // 获取包的名字 并进行替换  
  37.         String packageName = resourcePath;  
  38.         String packageDirName = packageName.replace('.''/');  
  39.         // 定义一个枚举的集合 并进行循环来处理这个目录下的things  
  40.         Enumeration dirs;  
  41.         try {  
  42.             dirs = Thread.currentThread().getContextClassLoader()  
  43.                     .getResources(packageDirName);  
  44.             // 循环迭代下去  
  45.             while (dirs.hasMoreElements()) {  
  46.                 // 获取下一个元素  
  47.                 URL url = dirs.nextElement();  
  48.                 // 得到协议的名称  
  49.                 String protocol = url.getProtocol();  
  50.                 // 如果是以文件的形式保存在服务器上  
  51.                 if ("file".equals(protocol)) {  
  52.                     // System.err.println("file类型的扫描");  
  53.                     // 获取包的物理路径  
  54.                     String filePath = URLDecoder.decode(url.getFile(), "UTF-8");  
  55.                     // 以文件的方式扫描整个包下的文件 并添加到集合中  
  56.                     findAndAddClassesInPackageByFile(packageName, filePath,  
  57.                             recursive, classes, scannerHandle);  
  58.                 } else if ("jar".equals(protocol)) {  
  59.                     // 如果是jar包文件  
  60.                     // 定义一个JarFile  
  61.                     // System.err.println("jar类型的扫描");  
  62.                     JarFile jar;  
  63.                     try {  
  64.                         // 获取jar  
  65.                         jar = ((JarURLConnection) url.openConnection())  
  66.                                 .getJarFile();  
  67.                         // 从此jar包 得到一个枚举类  
  68.                         Enumeration entries = jar.entries();  
  69.                         // 同样的进行循环迭代  
  70.                         while (entries.hasMoreElements()) {  
  71.                             // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件  
  72.                             JarEntry entry = entries.nextElement();  
  73.                             String name = entry.getName();  
  74.                             // 如果是以/开头的  
  75.                             if (name.charAt(0) == '/') {  
  76.                                 // 获取后面的字符串  
  77.                                 name = name.substring(1);  
  78.                             }  
  79.                             // 如果前半部分和定义的包名相同  
  80.                             if (name.startsWith(packageDirName)) {  
  81.                                 int idx = name.lastIndexOf('/');  
  82.                                 // 如果以"/"结尾 是一个包  
  83.                                 if (idx != -1) {  
  84.                                     // 获取包名 把"/"替换成"."  
  85.                                     packageName = name.substring(0, idx)  
  86.                                             .replace('/''.');  
  87.                                 }  
  88.                                 // 如果可以迭代下去 并且是一个包  
  89.                                 if ((idx != -1) || recursive) {  
  90.                                     // 如果是一个.class文件 而且不是目录  
  91.                                     if (name.endsWith(".class")  
  92.                                             && !entry.isDirectory()) {  
  93.                                         // 去掉后面的".class" 获取真正的类名  
  94.                                         // String className = name.substring(  
  95.                                         // packageName.length() + 1,  
  96.                                         // name.length() - 6);  
  97.                                         ClassMetadata classMetadata = getClassMetadata(  
  98.                                                 jar.getInputStream(entry));  
  99.                                         if (scannerHandle != null) {  
  100.                                             scannerHandle.handle(classMetadata);  
  101.                                         }  
  102.                                         // 添加到classes  
  103.                                         classes.add(classMetadata);  
  104.                                     }  
  105.                                 }  
  106.                             }  
  107.                         }  
  108.                     } catch (IOException e) {  
  109.                         // log.error("在扫描用户定义视图时从jar包获取文件出错");  
  110.                         e.printStackTrace();  
  111.                     }  
  112.                 }  
  113.             }  
  114.         } catch (IOException e) {  
  115.             e.printStackTrace();  
  116.         }  
  117.   
  118.         return classes;  
  119.     }  
  120.   
  121.     /** 
  122.      * 以文件的形式来获取包下的所有Class 
  123.      *  
  124.      * @param packageName 
  125.      * @param packagePath 
  126.      * @param recursive 
  127.      * @param classes 
  128.      * @throws Exception 
  129.      */  
  130.     private static void findAndAddClassesInPackageByFile(  
  131.             String packageName,  
  132.             String packagePath,  
  133.             final boolean recursive,  
  134.             Set classes,  
  135.             ScannerHandle scannerHandle)  
  136.             throws Exception {  
  137.         // 获取此包的目录 建立一个File  
  138.         File dir = new File(packagePath);  
  139.         // 如果不存在或者 也不是目录就直接返回  
  140.         if (!dir.exists() || !dir.isDirectory()) {  
  141.             // log.warn("用户定义包名 " + packageName + " 下没有任何文件");  
  142.             return;  
  143.         }  
  144.         // 如果存在 就获取包下的所有文件 包括目录  
  145.         File[] dirfiles = dir.listFiles(new FileFilter() {  
  146.             // 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)  
  147.             public boolean accept(File file) {  
  148.                 return (recursive && file.isDirectory())  
  149.                         || (file.getName().endsWith(".class"));  
  150.             }  
  151.         });  
  152.         // 循环所有文件  
  153.         for (File file : dirfiles) {  
  154.             // 如果是目录 则继续扫描  
  155.             if (file.isDirectory()) {  
  156.                 findAndAddClassesInPackageByFile(  
  157.                         packageName + "." + file.getName(),  
  158.                         file.getAbsolutePath(), recursive, classes,  
  159.                         scannerHandle);  
  160.             } else {  
  161.                 // 如果是java类文件 去掉后面的.class 只留下类名  
  162.                 // String className = file.getName().substring(0,  
  163.                 // file.getName().length() - 6);  
  164.                 ClassMetadata classMetadata = getClassMetadata(  
  165.                         new FileInputStream(file));  
  166.                 if (scannerHandle != null) {  
  167.                     scannerHandle.handle(classMetadata);  
  168.                 }  
  169.                 // 添加到classes  
  170.                 classes.add(classMetadata);  
  171.             }  
  172.         }  
  173.     }  
  174.   
  175.     /** 
  176.      * 返回类的元数据信息 
  177.      *  
  178.      * @param className 
  179.      * @return 
  180.      * @throws Exception 
  181.      */  
  182.     @SuppressWarnings("unused")  
  183.     @Deprecated  
  184.     private static ClassMetadata getClassMetadata(String className)  
  185.             throws Exception {  
  186.         ClassReader cr = new ClassReader(className);// ClassReader只是按顺序遍历一遍class文件内容,基本不做信息的缓存  
  187.         ClassMetadataVisitor cn = new ClassMetadataVisitor(Opcodes.ASM4);  
  188.         cr.accept(cn, ClassReader.SKIP_DEBUG);  
  189.         return cn;  
  190.     }  
  191.   
  192.     /** 
  193.      * 返回类的元数据信息 
  194.      * 
  195.      * @return 
  196.      * @throws Exception 
  197.      */  
  198.     private static ClassMetadata getClassMetadata(InputStream inputStream)  
  199.             throws Exception {  
  200.         try {  
  201.             ClassReader cr = new ClassReader(inputStream);// ClassReader只是按顺序遍历一遍class文件内容,基本不做信息的缓存  
  202.             ClassMetadataVisitor cn = new ClassMetadataVisitor(Opcodes.ASM4);  
  203.             cr.accept(cn, ClassReader.SKIP_DEBUG);  
  204.             return cn;  
  205.         } finally {  
  206.             if (inputStream != null) {  
  207.                 inputStream.close();  
  208.             }  
  209.         }  
  210.     }  
  211.   
  212. }  
  213.   
  214.   
  215. package test.util.classreading;  
  216.   
  217. public interface ScannerHandle {  
  218.   
  219.     void handle(ClassMetadata classMetadata) throws Exception;  
  220. }  
  221.   
  222.   
  223. package test.util.classreading;  
  224.   
  225.   
  226. public interface ClassMetadata {  
  227.   
  228.   
  229.     /** 
  230.      * Return the name of the underlying class. 
  231.      */  
  232.     String getClassName();  
  233.   
  234.     /** 
  235.      * Return whether the underlying class represents an interface. 
  236.      */  
  237.     boolean isInterface();  
  238.   
  239.     /** 
  240.      * Return whether the underlying class is marked as abstract. 
  241.      */  
  242.     boolean isAbstract();  
  243.   
  244.     /** 
  245.      * Return whether the underlying class represents a concrete class, 
  246.      * i.e. neither an interface nor an abstract class. 
  247.      */  
  248.     boolean isConcrete();  
  249.   
  250.     /** 
  251.      * Return whether the underlying class is marked as 'final'. 
  252.      */  
  253.     boolean isFinal();  
  254.   
  255.     /** 
  256.      * Determine whether the underlying class is independent, 
  257.      * i.e. whether it is a top-level class or a nested class 
  258.      * (static inner class) that can be constructed independent 
  259.      * from an enclosing class. 
  260.      */  
  261.     boolean isIndependent();  
  262.   
  263.     /** 
  264.      * Return whether the underlying class has an enclosing class 
  265.      * (i.e. the underlying class is an inner/nested class or 
  266.      * a local class within a method). 
  267.      * 

    If this method returns {@code false}, then the 

  268.      * underlying class is a top-level class. 
  269.      */  
  270.     boolean hasEnclosingClass();  
  271.   
  272.     /** 
  273.      * Return the name of the enclosing class of the underlying class, 
  274.      * or {@code null} if the underlying class is a top-level class. 
  275.      */  
  276.     String getEnclosingClassName();  
  277.   
  278.     /** 
  279.      * Return whether the underlying class has a super class. 
  280.      */  
  281.     boolean hasSuperClass();  
  282.   
  283.     /** 
  284.      * Return the name of the super class of the underlying class, 
  285.      * or {@code null} if there is no super class defined. 
  286.      */  
  287.     String getSuperClassName();  
  288.   
  289.     /** 
  290.      * Return the names of all interfaces that the underlying class 
  291.      * implements, or an empty array if there are none. 
  292.      */  
  293.     String[] getInterfaceNames();  
  294.   
  295.     /** 
  296.      * Return the names of all classes declared as members of the class represented by 
  297.      * this ClassMetadata object. This includes public, protected, default (package) 
  298.      * access, and private classes and interfaces declared by the class, but excludes 
  299.      * inherited classes and interfaces. An empty array is returned if no member classes 
  300.      * or interfaces exist. 
  301.      */  
  302.     String[] getMemberClassNames();  
  303.   
  304.   
  305.   
  306. }  
  307.   
  308.   
  309. package test.util.classreading;  
  310.   
  311. import java.util.LinkedHashSet;  
  312. import java.util.Set;  
  313.   
  314. import org.objectweb.asm.ClassVisitor;  
  315. import org.objectweb.asm.Opcodes;  
  316.   
  317. public class ClassMetadataVisitor extends ClassVisitor implements Opcodes,  
  318.         ClassMetadata {  
  319.   
  320.     private String className;  
  321.   
  322.     private boolean isInterface;  
  323.   
  324.     private boolean isAbstract;  
  325.   
  326.     private boolean isFinal;  
  327.   
  328.     private String enclosingClassName;  
  329.   
  330.     private boolean independentInnerClass;  
  331.   
  332.     private String superClassName;  
  333.   
  334.     private String[] interfaces;  
  335.   
  336.     private Set memberClassNames = new LinkedHashSet();  
  337.   
  338.     public ClassMetadataVisitor(int api) {  
  339.         super(api);  
  340.     }  
  341.   
  342.     public void visit(int version, int access, String name, String signature,  
  343.             String superName, String[] interfaces) {  
  344.         this.className = this.convertResourcePathToClassName(name);  
  345.         this.isInterface = ((access & Opcodes.ACC_INTERFACE) != 0);  
  346.         this.isAbstract = ((access & Opcodes.ACC_ABSTRACT) != 0);  
  347.         this.isFinal = ((access & Opcodes.ACC_FINAL) != 0);  
  348.         if (superName != null) {  
  349.             this.superClassName = this  
  350.                     .convertResourcePathToClassName(superName);  
  351.         }  
  352.         this.interfaces = new String[interfaces.length];  
  353.         for (int i = 0; i < interfaces.length; i++) {  
  354.             this.interfaces[i] = this  
  355.                     .convertResourcePathToClassName(interfaces[i]);  
  356.         }  
  357.     }  
  358.   
  359.     public void visitOuterClass(String owner, String name, String desc) {  
  360.         this.enclosingClassName = this.convertResourcePathToClassName(owner);  
  361.     }  
  362.   
  363.     public void visitInnerClass(String name, String outerName,  
  364.             String innerName, int access) {  
  365.         if (outerName != null) {  
  366.             String fqName = this.convertResourcePathToClassName(name);  
  367.             String fqOuterName = this.convertResourcePathToClassName(outerName);  
  368.             if (this.className.equals(fqName)) {  
  369.                 this.enclosingClassName = fqOuterName;  
  370.                 this.independentInnerClass = ((access & Opcodes.ACC_STATIC) != 0);  
  371.             } else if (this.className.equals(fqOuterName)) {  
  372.                 this.memberClassNames.add(fqName);  
  373.             }  
  374.         }  
  375.     }  
  376.   
  377.     public String convertResourcePathToClassName(String resourcePath) {  
  378.         return resourcePath.replace('/''.');  
  379.     }  
  380.   
  381.     @Override  
  382.     public String getClassName() {  
  383.         return this.className;  
  384.     }  
  385.   
  386.     @Override  
  387.     public boolean isInterface() {  
  388.         return this.isInterface;  
  389.     }  
  390.   
  391.     @Override  
  392.     public boolean isAbstract() {  
  393.         return this.isAbstract;  
  394.     }  
  395.   
  396.     @Override  
  397.     public boolean isConcrete() {  
  398.         return !(this.isInterface || this.isAbstract);  
  399.     }  
  400.   
  401.     @Override  
  402.     public boolean isFinal() {  
  403.         return this.isFinal;  
  404.     }  
  405.   
  406.     @Override  
  407.     public boolean isIndependent() {  
  408.         return (this.enclosingClassName == null || this.independentInnerClass);  
  409.     }  
  410.   
  411.     @Override  
  412.     public boolean hasEnclosingClass() {  
  413.         return (this.enclosingClassName != null);  
  414.     }  
  415.   
  416.     @Override  
  417.     public String getEnclosingClassName() {  
  418.         return this.enclosingClassName;  
  419.     }  
  420.   
  421.     @Override  
  422.     public boolean hasSuperClass() {  
  423.         return (this.superClassName != null);  
  424.     }  
  425.   
  426.     @Override  
  427.     public String getSuperClassName() {  
  428.         return this.superClassName;  
  429.     }  
  430.   
  431.     @Override  
  432.     public String[] getInterfaceNames() {  
  433.         return this.interfaces;  
  434.     }  
  435.   
  436.     @Override  
  437.     public String[] getMemberClassNames() {  
  438.         return this.memberClassNames.toArray(new String[this.memberClassNames  
  439.                 .size()]);  
  440.     }  
  441.   
  442. }  
具体的原理就是扫描文件和jar包里的class文件,用asm把class文件里的相关内容读取出来然后再交给handler进行操作。有人会问,干嘛用asm?,直接Class.forName()就完了?这里的原因有两点:1、是你加载的class可能会依赖别的包,但可能那个包并不在你的lib中,2、jvm是按需加载class的,你全都加载了,你的方法区(持久带)有多大?够放得下吗?就算是jdk8改成了直接内存,也得悠着点用。

说多了,这里扫描到了所有@ApiOperation注解和@Snippet注解的方法,然后把@Snippet注解里内容读出来,放map里备用。

然后,我们要用junit了:

[java]  view plain  copy
 
  1. package doc;  
  2.   
  3. import test.controller.WebConfiguration;  
  4. import doc.base.AbstractSwagger2Markup;  
  5. import doc.base.SwaggerConfig;  
  6. import io.github.robwin.markup.builder.MarkupLanguage;  
  7. import io.github.robwin.swagger2markup.GroupBy;  
  8. import io.github.robwin.swagger2markup.Swagger2MarkupConverter;  
  9. import org.junit.Before;  
  10. import org.junit.Rule;  
  11. import org.junit.Test;  
  12. import org.junit.runner.RunWith;  
  13. import org.springframework.beans.factory.annotation.Autowired;  
  14. import org.springframework.boot.test.context.SpringBootTest;  
  15. import org.springframework.http.MediaType;  
  16. import org.springframework.restdocs.JUnitRestDocumentation;  
  17. import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;  
  18. import org.springframework.test.context.web.WebAppConfiguration;  
  19. import org.springframework.test.web.servlet.MockMvc;  
  20. import org.springframework.test.web.servlet.MvcResult;  
  21. import org.springframework.test.web.servlet.setup.MockMvcBuilders;  
  22. import org.springframework.web.context.WebApplicationContext;  
  23. import springfox.documentation.staticdocs.SwaggerResultHandler;  
  24.   
  25. import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;  
  26. import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;  
  27. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;  
  28.   
  29.   
  30. @WebAppConfiguration  
  31. @RunWith(SpringJUnit4ClassRunner.class)  
  32. @SpringBootTest(classes = {  
  33.         WebConfiguration.class, SwaggerConfig.class  
  34. })  
  35. public class Swagger2Markup extends AbstractSwagger2Markup {  
  36.   
  37.     @Autowired  
  38.     private WebApplicationContext context;  
  39.   
  40.     private MockMvc mockMvc;  
  41.   
  42.   
  43.     @Rule  
  44.     public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(  
  45.             snippetsOutputDir);  
  46.   
  47.     @Before  
  48.     public void setUp() {  
  49.         this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)  
  50.                 .apply(documentationConfiguration(this.restDocumentation))  
  51.                 .build();  
  52.     }  
  53.   
  54.     /** 
  55.      * 生成所有接口的片断 
  56.      *  
  57.      * @throws Exception 
  58.      */  
  59.     @Test  
  60.     public void createSnippets() throws Exception {  
  61.         super.createSnippets(this.mockMvc);  
  62.     }  
  63. }  

McckMvc就是spring-restdoc的类,用来访问接口后成asciidoc用的,setUp方法定义了输出路径,最下面那个方法用得是super里的方法:

[java]  view plain  copy
 
  1. package doc.base;  
  2.   
  3. import lombok.extern.log4j.Log4j2;  
  4. import org.springframework.http.MediaType;  
  5. import org.springframework.test.web.servlet.MockMvc;  
  6. import org.springframework.test.web.servlet.ResultActions;  
  7. import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;  
  8.   
  9. import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;  
  10. import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;  
  11. import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;  
  12. import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;  
  13. import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;  
  14.   
  15. /** 
  16.  * 

     

  17.  * 所有Swagger2Markup类的父类 
  18.  * 

     
  19.  */  
  20. @Log4j2  
  21. public abstract class AbstractSwagger2Markup {  
  22.   
  23.     /** 
  24.      * 生成所的类的片段 
  25.      * 
  26.      * @param mockMvc 
  27.      *            MockMvc 
  28.      * @throws Exception 
  29.      */  
  30.     public void createSnippets(MockMvc mockMvc) throws Exception {  
  31.         ScanSnippet.snippetMap.forEach((K, V) -> {  
  32.             log.info("k={},v{}", K, V);  
  33.             String httpMethod = V.getHttpMethod();  
  34.             if (httpMethod != null) {  
  35.                 MockHttpServletRequestBuilder requestBuilder = null;  
  36.                 if (httpMethod.equalsIgnoreCase("get")) {  
  37.                     requestBuilder = get(  
  38.                             V.getURL());  
  39.                 } else if (httpMethod.equalsIgnoreCase("post")) {  
  40.                     requestBuilder = post(  
  41.                             V.getURL());  
  42.                 }  
  43.                 assert requestBuilder != null;  
  44.                 try {  
  45.                     log.info("开始生成" + K + "的片段");  
  46.                     if (V.getMediaType().equals(MediaType.APPLICATION_JSON)) {  
  47.                         ResultActions resultActions = mockMvc  
  48.                                 .perform(requestBuilder  
  49.                                         .content(V.getContent())  
  50.                                         .params(V.getParams())  
  51.                                         .headers(V.getHeaders())  
  52.                                         .cookie(V.getCookie())  
  53.                                         .contentType(  
  54.                                                 MediaType.APPLICATION_JSON))  
  55.                                 .andDo(document(K,  
  56.                                         preprocessResponse(prettyPrint())));  
  57.                         // resultActions.andExpect(status().isOk());  
  58.                     } else if (V.getMediaType()  
  59.                             .equals(MediaType.APPLICATION_FORM_URLENCODED)) {  
  60.                         ResultActions resultActions = mockMvc  
  61.                                 .perform(requestBuilder  
  62.                                         .params(V.getParams())  
  63.                                         .headers(V.getHeaders())  
  64.                                         .cookie(V.getCookie())  
  65.                                         .contentType(  
  66.                                                 MediaType.APPLICATION_FORM_URLENCODED))  
  67.                                 .andDo(document(K,  
  68.                                         preprocessResponse(prettyPrint())));  
  69.                         // resultActions.andExpect(status().isOk());  
  70.                     }  
  71.                     log.info("生成" + K + "的片段成功");  
  72.                 } catch (Exception e) {  
  73.                     log.error("生成" + K + "的片段失败:{}", e);  
  74.                 }  
  75.   
  76.             }  
  77.         });  
  78.     }  
  79.   
  80.     public String snippetsOutputDir = System  
  81.             .getProperty("io.springfox.staticdocs.snippetsOutputDir");// 片断目录  
  82.     public String outputDir = System  
  83.             .getProperty("io.springfox.staticdocs.outputDir");// swagger.json目录  
  84.     public String generatedOutputDir = System  
  85.             .getProperty("io.springfox.staticdocs.generatedOutputDir");// asciiDoc目录  
  86. }  

这个方法就是从刚才生成的map里得到所有的描述,一个一个的去访问,然后生成片段。

运行这个test会生成这些文件:

maven+spring-boot+springfox+swagger2markup+spring restdoc+asciidoctor生成完美的rest文档_第2张图片

4、swagger2markup(http://swagger2markup.github.io/swagger2markup/1.0.1/)

swagger2markup是一个专门用来转换swagger接口到markdown或acsiidoc的工具,可以把/v2/api-docs里得到的json转成markdown或acsiidoc格式。

[java]  view plain  copy
 
  1. @Test  
  2. public void createSpringfoxSwaggerJson() throws Exception {  
  3.   
  4.     // 得到swagger.json  
  5.     MvcResult mvcResult = this.mockMvc  
  6.             .perform(get("/v2/api-docs?group=test")  
  7.                     .accept(MediaType.APPLICATION_JSON))  
  8.             .andDo(SwaggerResultHandler.outputDirectory(outputDir).build())  
  9.             .andExpect(status().isOk())  
  10.             .andReturn();  
  11.   
  12.     // 转成asciiDoc,并加入Example  
  13.     Swagger2MarkupConverter.from(outputDir + "/swagger.json")  
  14.             .withPathsGroupedBy(GroupBy.TAGS)// 按tag排序  
  15.             .withMarkupLanguage(MarkupLanguage.ASCIIDOC)// 格式  
  16.             "color:#ff0000;">.withExamples(snippetsOutputDir)// 插入片断  
  17.             .build()  
  18.             .intoFolder(generatedOutputDir);// 输出  
  19. }  
这里访问了/v2/api-docs?group=test生成了test组的文档,同时,代码里红色的那句就是把刚才生成的片段插入到里面去。注意,目录要名字要和@ApiOperation中的value一样。


现在所有的东西都准备好了,但是我们一般不会看acsiidoc文件的。但可以生成HTML5,通过asciidoctor。


5、asciidoctor

asciidoctor有maven插件,可以自动把acsiidoc文件转成html和pdf,能自动生成目录,非常方便

先在创建这个文件:


文件内容是:

[html]  view plain  copy
 
  1. include::{generated}/overview.adoc[]  
  2. include::{generated}/definitions.adoc[]  
  3. include::{generated}/paths.adoc[]  
意思就是引入三个文件。

然后是maven插件:

[html]  view plain  copy
 
  1. <properties>  
  2.      <snippetsDirectory>${project.build.directory}/generated-snippetssnippetsDirectory>  
  3.   
  4.      <asciidoctor.input.directory>${project.basedir}/src/docs/asciidocasciidoctor.input.directory>  
  5.   
  6.      <swagger.output.dir>${project.build.directory}/swaggerswagger.output.dir>  
  7.      <swagger.snippetOutput.dir>${project.build.directory}/asciidoc/snippetsswagger.snippetOutput.dir>  
  8.      <generated.asciidoc.directory>${project.build.directory}/asciidoc/generatedgenerated.asciidoc.directory>  
  9.      <asciidoctor.html.output.directory>${project.build.directory}/asciidoc/htmlasciidoctor.html.output.directory>  
  10.      <asciidoctor.pdf.output.directory>${project.build.directory}/asciidoc/pdfasciidoctor.pdf.output.directory>  
  11.   
  12.      <swagger.input>${swagger.output.dir}/swagger.jsonswagger.input>  
  13.  properties>  
  14.   
  15.  <plugin>  
  16.              <groupId>org.asciidoctorgroupId>  
  17.              <artifactId>asciidoctor-maven-pluginartifactId>  
  18.              <version>1.5.3version>  
  19.                
  20.              <dependencies>  
  21.                  <dependency>  
  22.                      <groupId>org.asciidoctorgroupId>  
  23.                      <artifactId>asciidoctorj-pdfartifactId>  
  24.                      <version>1.5.0-alpha.10.1version>  
  25.                  dependency>  
  26.              dependencies>  
  27.                
  28.              <configuration>  
  29.                  <sourceDirectory>${asciidoctor.input.directory}sourceDirectory>  
  30.                  <sourceDocumentName>index.adocsourceDocumentName>  
  31.                  <attributes>  
  32.                      <doctype>bookdoctype>  
  33.                      <toc>lefttoc>  
  34.                      <toclevels>3toclevels>  
  35.                      <numbered>numbered>  
  36.                      <hardbreaks>hardbreaks>  
  37.                      <sectlinks>sectlinks>  
  38.                      <sectanchors>sectanchors>  
  39.                      <generated>${generated.asciidoc.directory}generated>  
  40.                  attributes>  
  41.              configuration>  
  42.              

你可能感兴趣的:(maven+spring-boot+springfox+swagger2markup+spring restdoc+asciidoctor生成完美的rest文档)