maven+spring-boot+springfox+swagger2markup+spring restdoc+asciidoctor生成完美的rest文档
2016-08-30 15:40
615 查看
写了一个工程,要写文档,相信所有的程序员都和我一样,最讨厌写文档,有没有片动生成文档的东西呢?有!
首先,我们要引入swagger。
我的项目是用spring-boot做的,基础知识就不在这里说了。只说怎么玩。
先是maven的引入:
我先写一个config类,看不懂的自己补下spring-boot:
然后,我们就可以在类上面加上swagger的注解了,只有这样,swagger才能生成文档:
然后我们调用一下http://localhost:8080/swagger-ui.html就可以看到了。
好了,我们现在可以用swagger-ui调试spring-mvc了,这只是第一步。
下面,我们要使用springfox生成文档。这里要使用swagger2markup来进行转换。
接口:
抽象实现类:
对应的实现类:
有的注解类,还要有一个读注解的类:
说多了,这里扫描到了所有@ApiOperation注解和@Snippet注解的方法,然后把@Snippet注解里内容读出来,放map里备用。
然后,我们要用junit了:
McckMvc就是spring-restdoc的类,用来访问接口后成asciidoc用的,setUp方法定义了输出路径,最下面那个方法用得是super里的方法:
这个方法就是从刚才生成的map里得到所有的描述,一个一个的去访问,然后生成片段。
运行这个test会生成这些文件:
现在所有的东西都准备好了,但是我们一般不会看acsiidoc文件的。但可以生成html5,通过asciidoctor。
先在创建这个文件:
文件内容是:
然后是maven插件:
现在我们只要运行mvn test,就可以得到最终的文档了:
最终效果:
spring-restdoc生成的例子部分:
首先,我们要引入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的引入:
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.5.0</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.5.0</version> </dependency> <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-mockmvc</artifactId> <version>1.1.1.RELEASE</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-staticdocs</artifactId> <version>2.5.0</version> <scope>test</scope> </dependency>
我先写一个config类,看不懂的自己补下spring-boot:
package doc.base; import lombok.extern.log4j.Log4j2; import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.context.EnvironmentAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.util.StopWatch; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import static springfox.documentation.builders.PathSelectors.*; import static com.google.common.base.Predicates.*; @Configuration @EnableSwagger2//注意这里 @ComponentScan(basePackages = "doc") @Log4j2 public class SwaggerConfig extends WebMvcConfigurerAdapter implements EnvironmentAware { /** * 静态资源映射 * * @param registry * 静态资源注册器 */ public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("swagger-ui.html") .addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/"); super.addResourceHandlers(registry); } @Override public void setEnvironment(Environment environment) {//这里是从配置文件里读相关的字段 this.propertyResolver = new RelaxedPropertyResolver(environment, "swagger."); } @Bean public Docket swaggerSpringfoxDocket4KAD() {//最重要的就是这里,定义了/test/.*开头的rest接口都分在了test分组里,可以通过/v2/api-docs?group=test得到定义的json log.debug("Starting Swagger"); StopWatch watch = new StopWatch(); watch.start(); Docket swaggerSpringMvcPlugin = new Docket(DocumentationType.SWAGGER_2) .groupName("test") .apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.any()) .paths(regex("/test/.*")) // and by paths .build(); watch.stop(); log.debug("Started Swagger in {} ms", watch.getTotalTimeMillis()); return swaggerSpringMvcPlugin; } private ApiInfo apiInfo() {//这里是生成文档基本信息的地方 return new ApiInfo(propertyResolver.getProperty("title"), propertyResolver.getProperty("description"), propertyResolver.getProperty("version"), propertyResolver.getProperty("termsOfServiceUrl"), new Contact(propertyResolver.getProperty("contact.name"), propertyResolver.getProperty("contact.url"), propertyResolver.getProperty("contact.email")), propertyResolver.getProperty("license"), propertyResolver.getProperty("licenseUrl")); } private RelaxedPropertyResolver propertyResolver; }由于spring-mvc代理了/*,所以要把swagger-ui.html和/webjars/**做为静态资源放出来,不然无法访问。
然后,我们就可以在类上面加上swagger的注解了,只有这样,swagger才能生成文档:
@ApiOperation( value = "get", httpMethod = "GET", response = String.class, notes = "调用test get", produces = MediaType.APPLICATION_JSON_VALUE)//这是接口的基本信息,不解释,自己看吧 @Snippet( url = "/test/get", snippetClass = MonitorControllerSnippet.Get.class)//这是我自己写的,方便spring-restdoc使用的,后面就说 @ApiImplicitParams({//这个是入参,因为入参是request,所以要在这里定义,如果是其它的比如spring或javabean入参,可以在参数上使用@ApiParam注解 @ApiImplicitParam( name = "Service", value = "服务", required = true, defaultValue = "monitor", dataType = "String"), @ApiImplicitParam( name = "Region", value = "机房", required = true, dataType = "String"), @ApiImplicitParam( name = "Version", value = "版本", required = true, dataType = "String"), @ApiImplicitParam( name = "name", value = "名称", example = "kaddefault", required = true, dataType = "String"), @ApiImplicitParam( name = "producttype", value = "产品类型", example = "12", required = true, dataType = "int"), @ApiImplicitParam( name = "tags", dataType = "String", example = "{\"port\":8080}") }) @RequestMapping( path = "/test/get", method = RequestMethod.GET) public String get(HttpServletRequest request) { log.debug("进入get"); return call4form(request); }
然后我们调用一下http://localhost:8080/swagger-ui.html就可以看到了。
好了,我们现在可以用swagger-ui调试spring-mvc了,这只是第一步。
下面,我们要使用springfox生成文档。这里要使用swagger2markup来进行转换。
3、spring restdoc
spring restdoc就是生成例子用的。先用它把每一个接口都调用一遍,会生成一堆acsiidoc文件。但是如果一个一个调,就把代码写死了,于是我写了一个自定的注解去完成这个工作:@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Snippet { String httpMethod() default "GET"; String url() default "/"; String mediaType() default "application/x-www-form-urlencoded"; // 用于生成片断的类,需要是test.doc.swagger.snippet.Snippet类的实现 Class snippetClass(); }
接口:
/** * <p> * 这是生成片断的方法所必须实现的接口 * </p> * Created by MiaoJia(miaojia@kingsoft.com) on 2016/8/26. */ public interface ISnippet { /** * 插入httpMethod * * @param httpMethod * GET or POST */ void setHttpMethod(String httpMethod); /** * 获取Http Method * * @return Http Method */ String getHttpMethod(); /** * 插入mediaType * * @param mediaType * application/x-www-form-urlencoded or application/json */ void setMediaType(String mediaType); /** * 获取MediaType * * @return MediaType */ MediaType getMediaType() ; /** * 插入URL * * @param url * URL */ void setURL(String url); /** * 获取URL * * @return url */ String getURL(); /** * 获取入参JSONs * * @return Json */ String getContent(); /** * 获取入参 * * @return MultiValueMap */ MultiValueMap<String, String> getParams(); /** * 得到头 * * @return HttpHeaders */ HttpHeaders getHeaders(); /** * 得到头Cookie * @return Cookie */ Cookie[] getCookie(); }
抽象实现类:
public abstract class ASnippet implements ISnippet { @Override public void setHttpMethod(String httpMethod) { this.httpMethod = httpMethod; } @Override public String getHttpMethod() { return httpMethod; } @Override public void setMediaType(String mediaType) { this.mediaType = MediaType.valueOf(mediaType); } @Override public MediaType getMediaType() { return mediaType; } @Override public void setURL(String url) { this.url = url; } @Override public String getURL() { return url; } @Override public HttpHeaders getHeaders() { return new HttpHeaders(); } @Override public String getContent() { return null; } @Override public Cookie[] getCookie() { return new Cookie[0]; } String httpMethod; MediaType mediaType; String url; }
对应的实现类:
public class MonitorControllerSnippet { /** * 抽象类 */ abstract static class BaseMonitorControllerSnippet extends ASnippet { public MultiValueMap<String, String> getParams() { MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(); parameters.put("Version", Collections.singletonList("2016-07-26")); parameters.put("Region", Collections.singletonList("cn-shanghai-3")); parameters.put("Service", Collections.singletonList("monitor")); return parameters; } @Override public Cookie[] getCookie() { Cookie cookie = new Cookie(PassportAPI.USER_TOKEN_KSCDIGEST, "046011086e3e617b98b7a6aa4cae88fc-668349870"); return new Cookie[] { cookie }; } } /** * get方法的 */ public static class Get extends BaseMonitorControllerSnippet { public MultiValueMap<String, String> getParams() { MultiValueMap<String, String> parameters = super.getParams(); parameters.put("name", Collections.singletonList("kaddefault")); parameters.put("instance", Collections .singletonList("0faae51b-e91f-4583-b83e-6b696d03d6b1")); parameters.put("producttype", Collections.singletonList("12")); return parameters; } } }针对上面这段代码,我解释的不是很清楚,导致有些同学存在疑问。其实,这段代码里就是我们要传给get这个接口的相关参数,这些参数可以根据自已接口的情况灵活可以增减,如果没有的话,就什么也不用写。ASnippet类里有很多实现,需要哪个就覆盖哪个。
有的注解类,还要有一个读注解的类:
@Component @Log4j2 public class ScanSnippet { /** * 查询所有的拥有@ApiOperation注解和@Snippet注解的方法,找到@Snippet注解中定义的snippetClass,放入缓存备用 * * @param basePackages 扫描路径 * @return 扫描到的类 */ private void doScan(String basePackages) throws Exception { ScanUtils.scanner(basePackages, classMetadata -> { Class beanClass = this.getClass().getClassLoader() .loadClass(classMetadata.getClassName()); for (Method method : beanClass.getMethods()) { ApiOperation apiOperation = method .getAnnotation(ApiOperation.class); Snippet snippet = method.getAnnotation(Snippet.class); if (apiOperation != null && snippet != null) { String apiName = apiOperation.value(); Class snippetClass = snippet.snippetClass(); if (ISnippet.class.isAssignableFrom(snippetClass)) { try { ISnippet _snippet = (ISnippet) snippetClass .newInstance(); _snippet.setHttpMethod(snippet.httpMethod()); _snippet.setMediaType(snippet.mediaType()); _snippet.setURL(snippet.url()); log.info("扫描到了:apiName={},_snippet={}", apiName, _snippet); snippetMap.put(apiName, _snippet); } catch (InstantiationException | IllegalAccessException e) { e.printStackTrace(); } } } } }); } /** * 启动时扫描 */ @PostConstruct public void scanSnippetMethod() { try { this.doScan("test"); } catch (Exception e) { e.printStackTrace(); } } /** * snippetMap */ public final static Map<String, ISnippet> snippetMap = new HashMap<>();这里用了扫描:
package test.util.classreading; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.JarURLConnection; import java.net.URL; import java.net.URLDecoder; import java.util.Enumeration; import java.util.LinkedHashSet; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.objectweb.asm.ClassReader; import org.objectweb.asm.Opcodes; public class ScanUtils { /** * 从包package中获取所有的Class * * @return * @throws Exception */ public static Set<ClassMetadata> scanner( String resourcePath, ScannerHandle scannerHandle) throws Exception { // 第一个class类的集合 Set<ClassMetadata> classes = new LinkedHashSet<ClassMetadata>(); // 是否循环迭代 boolean recursive = true; // 获取包的名字 并进行替换 String packageName = resourcePath; String packageDirName = packageName.replace('.', '/'); // 定义一个枚举的集合 并进行循环来处理这个目录下的things Enumeration<URL> dirs; try { dirs = Thread.currentThread().getContextClassLoader() .getResources(packageDirName); // 循环迭代下去 while (dirs.hasMoreElements()) { // 获取下一个元素 URL url = dirs.nextElement(); // 得到协议的名称 String protocol = url.getProtocol(); // 如果是以文件的形式保存在服务器上 if ("file".equals(protocol)) { // System.err.println("file类型的扫描"); // 获取包的物理路径 String filePath = URLDecoder.decode(url.getFile(), "UTF-8"); // 以文件的方式扫描整个包下的文件 并添加到集合中 findAndAddClassesInPackageByFile(packageName, filePath, recursive, classes, scannerHandle); } else if ("jar".equals(protocol)) { // 如果是jar包文件 // 定义一个JarFile // System.err.println("jar类型的扫描"); JarFile jar; try { // 获取jar jar = ((JarURLConnection) url.openConnection()) .getJarFile(); // 从此jar包 得到一个枚举类 Enumeration<JarEntry> entries = jar.entries(); // 同样的进行循环迭代 while (entries.hasMoreElements()) { // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件 JarEntry entry = entries.nextElement(); String name = entry.getName(); // 如果是以/开头的 if (name.charAt(0) == '/') { // 获取后面的字符串 name = name.substring(1); } // 如果前半部分和定义的包名相同 if (name.startsWith(packageDirName)) { int idx = name.lastIndexOf('/'); // 如果以"/"结尾 是一个包 if (idx != -1) { // 获取包名 把"/"替换成"." packageName = name.substring(0, idx) .replace('/', '.'); } // 如果可以迭代下去 并且是一个包 if ((idx != -1) || recursive) { // 如果是一个.class文件 而且不是目录 if (name.endsWith(".class") && !entry.isDirectory()) { // 去掉后面的".class" 获取真正的类名 // String className = name.substring( // packageName.length() + 1, // name.length() - 6); ClassMetadata classMetadata = getClassMetadata( jar.getInputStream(entry)); if (scannerHandle != null) { scannerHandle.handle(classMetadata); } // 添加到classes classes.add(classMetadata); } } } } } catch (IOException e) { // log.error("在扫描用户定义视图时从jar包获取文件出错"); e.printStackTrace(); } } } } catch (IOException e) { e.printStackTrace(); } return classes; } /** * 以文件的形式来获取包下的所有Class * * @param packageName * @param packagePath * @param recursive * @param classes * @throws Exception */ private static void findAndAddClassesInPackageByFile( String packageName, String packagePath, final boolean recursive, Set<ClassMetadata> classes, ScannerHandle scannerHandle) throws Exception { // 获取此包的目录 建立一个File File dir = new File(packagePath); // 如果不存在或者 也不是目录就直接返回 if (!dir.exists() || !dir.isDirectory()) { // log.warn("用户定义包名 " + packageName + " 下没有任何文件"); return; } // 如果存在 就获取包下的所有文件 包括目录 File[] dirfiles = dir.listFiles(new FileFilter() { // 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件) public boolean accept(File file) { return (recursive && file.isDirectory()) || (file.getName().endsWith(".class")); } }); // 循环所有文件 for (File file : dirfiles) { // 如果是目录 则继续扫描 if (file.isDirectory()) { findAndAddClassesInPackageByFile( packageName + "." + file.getName(), file.getAbsolutePath(), recursive, classes, scannerHandle); } else { // 如果是java类文件 去掉后面的.class 只留下类名 // String className = file.getName().substring(0, // file.getName().length() - 6); ClassMetadata classMetadata = getClassMetadata( new FileInputStream(file)); if (scannerHandle != null) { scannerHandle.handle(classMetadata); } // 添加到classes classes.add(classMetadata); } } } /** * 返回类的元数据信息 * * @param className * @return * @throws Exception */ @SuppressWarnings("unused") @Deprecated private static ClassMetadata getClassMetadata(String className) throws Exception { ClassReader cr = new ClassReader(className);// ClassReader只是按顺序遍历一遍class文件内容,基本不做信息的缓存 ClassMetadataVisitor cn = new ClassMetadataVisitor(Opcodes.ASM4); cr.accept(cn, ClassReader.SKIP_DEBUG); return cn; } /** * 返回类的元数据信息 * * @return * @throws Exception */ private static ClassMetadata getClassMetadata(InputStream inputStream) throws Exception { try { ClassReader cr = new ClassReader(inputStream);// ClassReader只是按顺序遍历一遍class文件内容,基本不做信息的缓存 ClassMetadataVisitor cn = new ClassMetadataVisitor(Opcodes.ASM4); cr.accept(cn, ClassReader.SKIP_DEBUG); return cn; } finally { if (inputStream != null) { inputStream.close(); } } } } package test.util.classreading; public interface ScannerHandle { void handle(ClassMetadata classMetadata) throws Exception; } package test.util.classreading; public interface ClassMetadata { /** * Return the name of the underlying class. */ String getClassName(); /** * Return whether the underlying class represents an interface. */ boolean isInterface(); /** * Return whether the underlying class is marked as abstract. */ boolean isAbstract(); /** * Return whether the underlying class represents a concrete class, * i.e. neither an interface nor an abstract class. */ boolean isConcrete(); /** * Return whether the underlying class is marked as 'final'. */ boolean isFinal(); /** * Determine whether the underlying class is independent, * i.e. whether it is a top-level class or a nested class * (static inner class) that can be constructed independent * from an enclosing class. */ boolean isIndependent(); /** * Return whether the underlying class has an enclosing class * (i.e. the underlying class is an inner/nested class or * a local class within a method). * <p>If this method returns {@code false}, then the * underlying class is a top-level class. */ boolean hasEnclosingClass(); /** * Return the name of the enclosing class of the underlying class, * or {@code null} if the underlying class is a top-level class. */ String getEnclosingClassName(); /** * Return whether the underlying class has a super class. */ boolean hasSuperClass(); /** * Return the name of the super class of the underlying class, * or {@code null} if there is no super class defined. */ String getSuperClassName(); /** * Return the names of all interfaces that the underlying class * implements, or an empty array if there are none. */ String[] getInterfaceNames(); /** * Return the names of all classes declared as members of the class represented by * this ClassMetadata object. This includes public, protected, default (package) * access, and private classes and interfaces declared by the class, but excludes * inherited classes and interfaces. An empty array is returned if no member classes * or interfaces exist. */ String[] getMemberClassNames(); } package test.util.classreading; import java.util.LinkedHashSet; import java.util.Set; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Opcodes; public class ClassMetadataVisitor extends ClassVisitor implements Opcodes, ClassMetadata { private String className; private boolean isInterface; private boolean isAbstract; private boolean isFinal; private String enclosingClassName; private boolean independentInnerClass; private String superClassName; private String[] interfaces; private Set<String> memberClassNames = new LinkedHashSet<String>(); public ClassMetadataVisitor(int api) { super(api); } public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { this.className = this.convertResourcePathToClassName(name); this.isInterface = ((access & Opcodes.ACC_INTERFACE) != 0); this.isAbstract = ((access & Opcodes.ACC_ABSTRACT) != 0); this.isFinal = ((access & Opcodes.ACC_FINAL) != 0); if (superName != null) { this.superClassName = this .convertResourcePathToClassName(superName); } this.interfaces = new String[interfaces.length]; for (int i = 0; i < interfaces.length; i++) { this.interfaces[i] = this .convertResourcePathToClassName(interfaces[i]); } } public void visitOuterClass(String owner, String name, String desc) { this.enclosingClassName = this.convertResourcePathToClassName(owner); } public void visitInnerClass(String name, String outerName, String innerName, int access) { if (outerName != null) { String fqName = this.convertResourcePathToClassName(name); String fqOuterName = this.convertResourcePathToClassName(outerName); if (this.className.equals(fqName)) { this.enclosingClassName = fqOuterName; this.independentInnerClass = ((access & Opcodes.ACC_STATIC) != 0); } else if (this.className.equals(fqOuterName)) { this.memberClassNames.add(fqName); } } } public String convertResourcePathToClassName(String resourcePath) { return resourcePath.replace('/', '.'); } @Override public String getClassName() { return this.className; } @Override public boolean isInterface() { return this.isInterface; } @Override public boolean isAbstract() { return this.isAbstract; } @Override public boolean isConcrete() { return !(this.isInterface || this.isAbstract); } @Override public boolean isFinal() { return this.isFinal; } @Override public boolean isIndependent() { return (this.enclosingClassName == null || this.independentInnerClass); } @Override public boolean hasEnclosingClass() { return (this.enclosingClassName != null); } @Override public String getEnclosingClassName() { return this.enclosingClassName; } @Override public boolean hasSuperClass() { return (this.superClassName != null); } @Override public String getSuperClassName() { return this.superClassName; } @Override public String[] getInterfaceNames() { return this.interfaces; } @Override public String[] getMemberClassNames() { return this.memberClassNames.toArray(new String[this.memberClassNames .size()]); } }具体的原理就是扫描文件和jar包里的class文件,用asm把class文件里的相关内容读取出来然后再交给handler进行操作。有人会问,干嘛用asm?,直接Class.forName()就完了?这里的原因有两点:1、是你加载的class可能会依赖别的包,但可能那个包并不在你的lib中,2、jvm是按需加载class的,你全都加载了,你的方法区(持久带)有多大?够放得下吗?就算是jdk8改成了直接内存,也得悠着点用。
说多了,这里扫描到了所有@ApiOperation注解和@Snippet注解的方法,然后把@Snippet注解里内容读出来,放map里备用。
然后,我们要用junit了:
package doc; import test.controller.WebConfiguration; import doc.base.AbstractSwagger2Markup; import doc.base.SwaggerConfig; import io.github.robwin.markup.builder.MarkupLanguage; import io.github.robwin.swagger2markup.GroupBy; import io.github.robwin.swagger2markup.Swagger2MarkupConverter; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import springfox.documentation.staticdocs.SwaggerResultHandler; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebAppConfiguration @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = { WebConfiguration.class, SwaggerConfig.class }) public class Swagger2Markup extends AbstractSwagger2Markup { @Autowired private WebApplicationContext context; private MockMvc mockMvc; @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( snippetsOutputDir); @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .build(); } /** * 生成所有接口的片断 * * @throws Exception */ @Test public void createSnippets() throws Exception { super.createSnippets(this.mockMvc); } }
McckMvc就是spring-restdoc的类,用来访问接口后成asciidoc用的,setUp方法定义了输出路径,最下面那个方法用得是super里的方法:
package doc.base; import lombok.extern.log4j.Log4j2; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; /** * <p> * 所有Swagger2Markup类的父类 * </p> */ @Log4j2 public abstract class AbstractSwagger2Markup { /** * 生成所的类的片段 * * @param mockMvc * MockMvc * @throws Exception */ public void createSnippets(MockMvc mockMvc) throws Exception { ScanSnippet.snippetMap.forEach((K, V) -> { log.info("k={},v{}", K, V); String httpMethod = V.getHttpMethod(); if (httpMethod != null) { MockHttpServletRequestBuilder requestBuilder = null; if (httpMethod.equalsIgnoreCase("get")) { requestBuilder = get( V.getURL()); } else if (httpMethod.equalsIgnoreCase("post")) { requestBuilder = post( V.getURL()); } assert requestBuilder != null; try { log.info("开始生成" + K + "的片段"); if (V.getMediaType().equals(MediaType.APPLICATION_JSON)) { ResultActions resultActions = mockMvc .perform(requestBuilder .content(V.getContent()) .params(V.getParams()) .headers(V.getHeaders()) .cookie(V.getCookie()) .contentType( MediaType.APPLICATION_JSON)) .andDo(document(K, preprocessResponse(prettyPrint()))); // resultActions.andExpect(status().isOk()); } else if (V.getMediaType() .equals(MediaType.APPLICATION_FORM_URLENCODED)) { ResultActions resultActions = mockMvc .perform(requestBuilder .params(V.getParams()) .headers(V.getHeaders()) .cookie(V.getCookie()) .contentType( MediaType.APPLICATION_FORM_URLENCODED)) .andDo(document(K, preprocessResponse(prettyPrint()))); // resultActions.andExpect(status().isOk()); } log.info("生成" + K + "的片段成功"); } catch (Exception e) { log.error("生成" + K + "的片段失败:{}", e); } } }); } public String snippetsOutputDir = System .getProperty("io.springfox.staticdocs.snippetsOutputDir");// 片断目录 public String outputDir = System .getProperty("io.springfox.staticdocs.outputDir");// swagger.json目录 public String generatedOutputDir = System .getProperty("io.springfox.staticdocs.generatedOutputDir");// asciiDoc目录 }
这个方法就是从刚才生成的map里得到所有的描述,一个一个的去访问,然后生成片段。
运行这个test会生成这些文件:
4、swagger2markup(http://swagger2markup.github.io/swagger2markup/1.0.1/)
swagger2markup是一个专门用来转换swagger接口到markdown或acsiidoc的工具,可以把/v2/api-docs里得到的json转成markdown或acsiidoc格式。@Test public void createSpringfoxSwaggerJson() throws Exception { // 得到swagger.json MvcResult mvcResult = this.mockMvc .perform(get("/v2/api-docs?group=test") .accept(MediaType.APPLICATION_JSON)) .andDo(SwaggerResultHandler.outputDirectory(outputDir).build()) .andExpect(status().isOk()) .andReturn(); // 转成asciiDoc,并加入Example Swagger2MarkupConverter.from(outputDir + "/swagger.json") .withPathsGroupedBy(GroupBy.TAGS)// 按tag排序 .withMarkupLanguage(MarkupLanguage.ASCIIDOC)// 格式 .withExamples(snippetsOutputDir)// 插入片断</span> .build() .intoFolder(generatedOutputDir);// 输出 }这里访问了/v2/api-docs?group=test生成了test组的文档,同时,代码里红色的那句就是把刚才生成的片段插入到里面去。注意,目录要名字要和@ApiOperation中的value一样。
现在所有的东西都准备好了,但是我们一般不会看acsiidoc文件的。但可以生成html5,通过asciidoctor。
5、asciidoctor
asciidoctor有maven插件,可以自动把acsiidoc文件转成html和pdf,能自动生成目录,非常方便先在创建这个文件:
文件内容是:
include::{generated}/overview.adoc[] include::{generated}/definitions.adoc[] include::{generated}/paths.adoc[]意思就是引入三个文件。
然后是maven插件:
<properties> <snippetsDirectory>${project.build.directory}/generated-snippets</snippetsDirectory> <asciidoctor.input.directory>${project.basedir}/src/docs/asciidoc</asciidoctor.input.directory> <swagger.output.dir>${project.build.directory}/swagger</swagger.output.dir> <swagger.snippetOutput.dir>${project.build.directory}/asciidoc/snippets</swagger.snippetOutput.dir> <generated.asciidoc.directory>${project.build.directory}/asciidoc/generated</generated.asciidoc.directory> <asciidoctor.html.output.directory>${project.build.directory}/asciidoc/html</asciidoctor.html.output.directory> <asciidoctor.pdf.output.directory>${project.build.directory}/asciidoc/pdf</asciidoctor.pdf.output.directory> <swagger.input>${swagger.output.dir}/swagger.json</swagger.input> </properties> <plugin> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctor-maven-plugin</artifactId> <version>1.5.3</version> <!-- Include Asciidoctor PDF for pdf generation --> <dependencies> <dependency> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctorj-pdf</artifactId> <version>1.5.0-alpha.10.1</version> </dependency> </dependencies> <!-- Configure generic document generation settings --> <configuration> <sourceDirectory>${asciidoctor.input.directory}</sourceDirectory> <sourceDocumentName>index.adoc</sourceDocumentName> <attributes> <doctype>book</doctype> <toc>left</toc> <toclevels>3</toclevels> <numbered></numbered> <hardbreaks></hardbreaks> <sectlinks></sectlinks> <sectanchors></sectanchors> <generated>${generated.asciidoc.directory}</generated> </attributes> </configuration> <!-- Since each execution can only handle one backend, run separate executions for each desired output type --> <executions> <execution> <id>output-html</id> <phase>test</phase> <goals> <goal>process-asciidoc</goal> </goals> <configuration> <backend>html5</backend> <outputDirectory>${asciidoctor.html.output.directory}</outputDirectory> </configuration> </execution> <!--<execution> <id>output-pdf</id> <phase>test</phase> <goals> <goal>process-asciidoc</goal> </goals> <configuration> <backend>pdf</backend> <outputDirectory>${asciidoctor.pdf.output.directory}</outputDirectory> </configuration> </execution>--> </executions> </plugin>
现在我们只要运行mvn test,就可以得到最终的文档了:
最终效果:
spring-restdoc生成的例子部分:
相关文章推荐
- Spring boot中使用MyBatis Generator
- 使用springfox 集成swagger 与spring mvc
- Spring boot Jetty 服务器相比tomcat的一些不足
- 使用了Spring boot devtools, dozer转换嵌套对象失败解决记录
- Springfox swagger-ui 覆盖默认request host
- Spring Boot学习笔记-快速示例
- Spring Boot学习笔记—日志
- Spring Boot学习笔记-SQL数据库使用
- Spring Boot学习笔记-错误处理及自定义
- Spring Boot学习笔记-外部化属性配置
- Springfox
- 使用Spring Boot Gradle 打war包的一点心得体会
- asciidoctor-pdf 可以使用的参数
- asciidoctor-pdf中文乱码问题或显示不全
- spring boot和mybatis整合
- Spring-boot生成可执行jar包
- 使用Swagger在SpringBoot项目中管理API文档(使用Oauth2)
- spring-boot 所有的配置
- spring boot log4j2配置(使用log4j2.yml文件)
- 漂亮打印Spring Boot Actuator端点的JSON输出