在前后端分离盛兴的时代,REST API是前后端通信的必要途径。一个好的REST API,不仅符合RESTful规范,还需要有一个高质量的API服务文档。对于API服务文档来讲,难点不是创建文档,而是如何更高效的维护文档,让文档和代码的变化保持同步。错误的API服务文档,会让使用者走入歧途,还不如没有文档的好。

Spring REST Docs

值得开心的是,在Spring全家桶中就用这样的工具来满足我们的要求。Spring REST Docs采用手写文档和自动生成片段相结合的方式来创建API文档。Asciidoc是手写文档的格式,这种文档格式在Spring全家桶中广广泛使用,Spring Reference document就是使用它生成的。

Spring REST Docs依据单元测试时所需的HTTP请求及其响应自动生成单元测试的文档。

配置

我们都知道现在流行的Java项目管理工具有Maven和Gradle两种。在Spring Framework的高级版本,将项目管理工具从Maven切换到了Gradle。因此,我们也采用Gradle来管理我们的项目。

build.gradle配置

buildscript {
   repositories {
       mavenCentral()
   }
   dependencies {
       classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.1.8.RELEASE'
   }
}

plugins {
   id "org.asciidoctor.convert" version "1.5.9.2"
}

apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'eclipse'
apply plugin: 'io.spring.dependency-management'

repositories {
   mavenLocal()
   maven { url 'https://repo.spring.io/libs-snapshot' }
   mavenCentral()
}


sourceCompatibility = 1.8
targetCompatibility = 1.8

ext {
   snippetsDir = file('build/generated-snippets')
}

ext['spring-restdocs.version'] = '2.0.5.BUILD-SNAPSHOT'

dependencies {
   asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor'

   compile 'org.springframework.boot:spring-boot-starter-web'

   testCompile('org.springframework.boot:spring-boot-starter-test') {
       exclude group: 'junit', module: 'junit;'
   }
   testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc'
   testCompile 'org.junit.jupiter:junit-jupiter-api'
   testRuntime 'org.junit.jupiter:junit-jupiter-engine'
}

test {
   outputs.dir snippetsDir
   useJUnitPlatform()
}

asciidoctor {
   inputs.dir snippetsDir
   dependsOn test
}

bootJar {
   dependsOn asciidoctor
   from("${asciidoctor.outputDir}/html5") {
       into 'static/docs'
   }
}

eclipseJdt.onlyIf { false }
cleanEclipseJdt.onlyIf { false }

在这里,我们的单元测试JUnit使用JUnit 5版本,它在Spring 5中得到了很好的支持。

像其他的Spring Boot项目一样,我们需要创建一个Application。

package io.github.tinyking.spotlighs.restdocs;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* RestdocsApplication.
*
* @author tinyking
* @version 1.0
* @since 2019/10/15
*/

@SpringBootApplication
public class RestdocsApplication {
   public static void main(String[] args) {
       SpringApplication.run(RestdocsApplication.class, args);
   }
}

创建一个简单的Controller,用来做的HTTP API测试

package io.github.tinyking.spotlighs.restdocs;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* SampleController.
*
* @author tinyking
* @version 1.0
* @since 2019/10/15
*/

@RestController
public class SampleController {

   @GetMapping("/")
   public String index() {
       return "Hello, World";
   }
}

接下来,我们需要为Application创建一个使用JUnit 5单元测试

package io.github.tinyking.spotlighs.restdocs;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
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;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

/**
* RestdocsApplicationTest.
*
* @author tinyking
* @version 1.0
* @since 2019/10/15
*/

@SpringBootTest
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
public class RestdocsApplicationTest {

   @Autowired
   private WebApplicationContext wac;

   private MockMvc mockMvc;

   @BeforeEach
   public void setup(RestDocumentationContextProvider restDocumentationContextProvider) {
       this.mockMvc = MockMvcBuilders.webAppContextSetup(wac)
               .apply(documentationConfiguration(restDocumentationContextProvider)).build();
   }

   @Test
   public void sample() throws Exception {
       this.mockMvc.perform(get("/"))
               .andExpect(status().isOk())
               .andDo(document("sample"));
   }
}

手写文档

就像一开始说的,我们需要手写文档+自动生成来实现REST API服务文档。上面所有的操作都是为了自动生成片段,下面我们来创建一个手写文档。

spotlight-spring-restdocs/src/docs/asciidoc/index.adoc

= 基于JUnit5的Spring restdocs实例
tinyking;
:doctype: book
:icons: font
:source-highlighter: highlightjs

Http curl请求示例:

include::{snippets}/sample/curl-request.adoc[]

Http请求:

include::{snippets}/sample/http-request.adoc[]

Http响应:

include::{snippets}/sample/http-response.adoc[]

这就是一个简单的手写+自动的文档,里面的curl-request.adoc、http-request.adoc、

http-response.adoc都是根据单元测试自动生成的。

生成API文档

接下来我们通过命名来生成API文档

gradle bootJar

执行完成后,可以找到build/asciidoc/html5/index.html ,直接在浏览器中打开,如下:

使用Spring REST docs & JUnit 5创建API服务文档_java

总结

通过使用Spring REST docs,可以让我们快速的生成API服务文档,基于单元测试可以确保API文档和代码保持一致。并且Spring REST Docs的方式不像Swagger,它不会入侵我们的业务代码,保证我们代码的整洁性。