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的引入:
- <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;
- }
然后,我们就可以在类上面加上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;
- }
- }
- }
有的注解类,还要有一个读注解的类:
- @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()]);
- }
- }
说多了,这里扫描到了所有@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)// 格式
- <span style="color:#ff0000;">.withExamples(snippetsOutputDir)// 插入片断</span>
- .build()
- .intoFolder(generatedOutputDir);// 输出
- }
现在所有的东西都准备好了,但是我们一般不会看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生成的例子部分: