写了一个工程,要写文档,相信所有的程序员都和我一样,最讨厌写文档,有没有片动生成文档的东西呢?有!
首先,我们要引入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的引入:
io.springfox
springfox-swagger-ui
2.5.0
io.springfox
springfox-swagger2
2.5.0
org.springframework.restdocs
spring-restdocs-mockmvc
1.1.1.RELEASE
io.springfox
springfox-staticdocs
2.5.0
test
我先写一个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);
}
好了,我们现在可以用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();
}
接口:
/**
*
* 这是生成片断的方法所必须实现的接口
*
* Created by MiaoJia([email protected]) 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 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 getParams() {
MultiValueMap 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 getParams() {
MultiValueMap 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 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 scanner(
String resourcePath,
ScannerHandle scannerHandle) throws Exception {
// 第一个class类的集合
Set classes = new LinkedHashSet();
// 是否循环迭代
boolean recursive = true;
// 获取包的名字 并进行替换
String packageName = resourcePath;
String packageDirName = packageName.replace('.', '/');
// 定义一个枚举的集合 并进行循环来处理这个目录下的things
Enumeration 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 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 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).
* 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 memberClassNames = new LinkedHashSet();
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);
}
}
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;
/**
*
* 所有Swagger2Markup类的父类
*
*/
@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格式。
@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)// 插入片断
.build()
.intoFolder(generatedOutputDir);// 输出
}
这里访问了/v2/api-docs?group=test生成了test组的文档,同时,代码里红色的那句就是把刚才生成的片段插入到里面去。注意,目录要名字要和@ApiOperation中的value一样。
现在所有的东西都准备好了,但是我们一般不会看acsiidoc文件的。但可以生成html5,通过asciidoctor。
asciidoctor有maven插件,可以自动把acsiidoc文件转成html和pdf,能自动生成目录,非常方便
先在创建这个文件:
文件内容是:
include::{generated}/overview.adoc[]
include::{generated}/definitions.adoc[]
include::{generated}/paths.adoc[]
意思就是引入三个文件。
然后是maven插件:
${project.build.directory}/generated-snippets
${project.basedir}/src/docs/asciidoc
${project.build.directory}/swagger
${project.build.directory}/asciidoc/snippets
${project.build.directory}/asciidoc/generated
${project.build.directory}/asciidoc/html
${project.build.directory}/asciidoc/pdf
${swagger.output.dir}/swagger.json
org.asciidoctor
asciidoctor-maven-plugin
1.5.3
org.asciidoctor
asciidoctorj-pdf
1.5.0-alpha.10.1
${asciidoctor.input.directory}
index.adoc
book
left
3
${generated.asciidoc.directory}
output-html
test
process-asciidoc
html5
${asciidoctor.html.output.directory}
最终效果:
spring-restdoc生成的例子部分: