写了一个工程,要写文档,相信所有的程序员都和我一样,最讨厌写文档,有没有片动生成文档的东西呢?有!
首先,我们要引入 swagger。
什么是 swagger?说白了,就是可以帮你生成一个可以测试接口的页面的工具。具体在这里:http://swagger.io/open-source-integrations/。多得我也不说了,文档很多,具体可以看这里:http://blog.sina.com.cn/s/blog_72ef7bea0102vpu7.html。说这个东西的的原因是,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:
由于 spring-mvc 代理了 /*,所以要把 swagger-ui.html 和 / webjars/** 做为静态资源放出来,不然无法访问。
- 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;
- }
然后,我们就可以在类上面加上 swagger 的注解了,只有这样,swagger 才能生成文档:
然后我们调用一下 http://localhost:8080/swagger-ui.html 就可以看到了。
- @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);
- }
好了,我们现在可以用 swagger-ui 调试 spring-mvc 了,这只是第一步。
下面,我们要使用 springfox 生成文档。这里要使用 swagger2markup 来进行转换。
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;
- }
- }
- }
有的注解类,还要有一个读注解的类:
这里用了扫描:
- @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 < >();
具体的原理就是扫描文件和 jar 包里的 class 文件,用 asm 把 class 文件里的相关内容读取出来然后再交给 handler 进行操作。有人会问,干嘛用 asm?,直接 Class.forName() 就完了?这里的原因有两点:1、是你加载的 class 可能会依赖别的包,但可能那个包并不在你的 lib 中,2、jvm 是按需加载 class 的,你全都加载了,你的方法区(持久带)有多大?够放得下吗?就算是 jdk8 改成了直接内存,也得悠着点用。
- 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()]);
- }
- }
说多了,这里扫描到了所有 @ApiOperation 注解和 @Snippet 注解的方法,然后把 @Snippet 注解里内容读出来,放 map 里备用。
然后,我们要用 junit 了:
McckMvc 就是 spring-restdoc 的类,用来访问接口后成 asciidoc 用的,setUp 方法定义了输出路径,最下面那个方法用得是 super 里的方法:
- 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);
- }
- }
- 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目录
- }
运行这个 test 会生成这些文件:
swagger2markup 是一个专门用来转换 swagger 接口到 markdown 或 acsiidoc 的工具,可以把 / v2/api-docs 里得到的 json 转成 markdown 或 acsiidoc 格式。
这里访问了 / v2/api-docs?group=test 生成了 test 组的文档,同时, 代码里红色的那句就是把刚才生成的片段插入到里面去。注意,目录要名字要和 @ApiOperation 中的 value 一样。
- @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) // 格式
- < span style = "color:#ff0000;" > .withExamples(snippetsOutputDir) // 插入片断</span>
- .build().intoFolder(generatedOutputDir); // 输出
- }
现在所有的东西都准备好了,但是我们一般不会看 acsiidoc 文件的。但可以生成 html5,通过 asciidoctor。
asciidoctor 有 maven 插件,可以自动把 acsiidoc 文件转成 html 和 pdf,能自动生成目录,非常方便
先在创建这个文件:
文件内容是:
意思就是引入三个文件。
- include: :{
- generated
- }
- /overview.adoc[]
- include::{generated}/definitions.adoc[] include: :{
- generated
- }
- /paths.adoc[]/
然后是 maven 插件:
现在我们只要运行 mvn test,就可以得到最终的文档了:
- <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>
最终效果:
spring-restdoc 生成的例子部分:
来源: http://lib.csdn.net/article/java/41985