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

原创 2016年08月30日 15:40:20

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

首先,我们要引入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-restdocs 自动生成接口文档

前言 Spring REST Docs helps you to document RESTful services. It combines hand-written documentation...

SpringBoot非官方教程 | 第十篇: 用spring Restdocs创建API文档

转载请标明出处:  http://blog.csdn.net/forezp/article/details/71023510  本文出自方志朋的博客 这篇文章将带你了解如何用spring官方推荐的...

SpringBoot非官方教程 | 第十篇: 用spring Restdocs创建API文档

转载请标明出处: http://blog.csdn.net/forezp/article/details/71023510 本文出自方志朋的博客 这篇文章将带你了解如何用spring官...
  • zwfmu
  • zwfmu
  • 2017年05月03日 22:48
  • 1241

maven native2ascii-maven-plugin 例子

framework/message                             org.codehaus.mojo                 native2ascii...

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

http://blog.csdn.net/daisy_xiu/article/details/52368920 写了一个工程,要写文档,相信所有的程序员都和我一样,最讨厌写文档,有没有片动生...

使用swagger作为restful api的doc文档生成

初衷 记得以前写接口,写完后会整理一份API接口文档,而文档的格式如果没有具体要求的话,最终展示的文档则完全决定于开发者的心情。也许多点,也许少点。甚至,接口总是需要适应新需求的,修改了,增加了...

Swagger2在header中添加token(java)

@Bean public Docket apiDocument() { return new Docket(DocumentationType.SWAGGER_2) ...
  • wanzhix
  • wanzhix
  • 2017年07月24日 14:03
  • 840

Spring Boot集成Swagger2并替换默认的UI

Spring Boot继承Swagger2并替换默认的UI,网上大神写的一个页面,美观大气上档次,自己下载下来稍微改了下界面并支持响应参数...
  • soanl
  • soanl
  • 2017年05月02日 11:24
  • 4002

swagger2 与 springmvc 整合 生成接口文档

简介 Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法,参数和模型紧密集...
  • BuddyUU
  • BuddyUU
  • 2016年12月20日 15:00
  • 1644

Spring Boot学习笔记 - 整合Swagger2自动生成RESTful API文档

在App后端开发中经常需要对移动客户端(Android、iOS)提供RESTful API接口,在后期版本快速迭代的过程中,修改接口实现的时候都必须同步修改接口文档,而文档与代码又处于两个不同的媒介,...
  • FX_SKY
  • FX_SKY
  • 2017年01月04日 19:28
  • 4151
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:maven+spring-boot+springfox+swagger2markup+spring restdoc+asciidoctor生成完美的rest文档
举报原因:
原因补充:

(最多只允许输入30个字)