0%

使用SpringRestDoc和AsciiDoc来构造项目文档

简介

程序员最讨厌的事情之一就是写文档,特别是word文档,写好word文档是并不是一件简单的事,以个人经验来看如果有人提供了一份word文档给我阅读,往往阅读效率是很低下的,于是催生出了类似于Markdown或者AsciiDoc这种以编程方式来写文档的工具,通过这些工具的语法,我们可以不需要学习各种排版样式而写入清新的文档,提到rest文档,不可绕开的就是Swagger,目前项目中全部使用了Swagger,但是使用Swagger的优缺点都很明显,优点:方便的工具让前端联调接口更简单,缺点:入侵式的注解让代码看起来杂乱无章,且文档不够直观等,在这些原因的驱使下让我去寻找一款更加清晰的文档工具,本人发现了2款文档工具 1:smart-doc 它是一款非入侵似的文档框架,采用类似java doc的标准注释来生成文档,这也强迫用户必须写注释,但是缺点是由于采用了注释,很多情况下不够灵活,第二个就是Spring Rest doc,来自Spring官方的Spring res doc, 生成的文档样式可以参考Spring的官方文档,其采用AsciiDoc作为编写文档的工具,美观,且更加灵活,并且其于单元测试强制绑定,也驱使我们培养出写单元测试的良好习惯,所以我这里采用Sprng rest doc来编写文档

效果图

图片

创建项目

  • demo代码https://github.com/liushprofessor/hase_repository/tree/master/spring_test

  • 以下是我创建的MAVEN工程的Pom文件,由于Spring rest doc 是和单元测试签字绑定所以我这里引入了Springtest的包,并且引入Sprig rest doc的包spring-restdocs-mockmvc

  • 我们重点来关注一下plugin,其定义了3个maven插件,spring-boot-maven-plugin没什么好说的,springboot打包插件,asciidoctor-maven-plugin其作用将为asciidoctor文档打包成html样式的文档,其绑定了maven的prepare-packet生命周期,在执行到该周期时自动将asciidoc的文件转成html,还有一个参数我们需要注意${snippetsDirectory},该配置规定了生成asciidoc文件的目录,第三个插件就是maven-resources-plugin,其同样绑定在prepare-packet什么周期,该插件的左右就是在打包时规定资源生成的路径,比如我这里将${project.build.directory}/generated-docs 目录下生成的html文件在打包时放入到static的docs文件下, 因为Maven规定 resources 下的static 目录是放置静态资源的目录,所以我们将生成的html放在该目录下,那么可以直接访问里面的文档.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.liu</groupId>
<artifactId>spring_test</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<snippetsDirectory>${project.build.directory}/generated-snippets</snippetsDirectory>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
<relativePath/>
</parent>

<dependencies>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>

</dependencies>


<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.8</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
<attributes>
<snippets>${snippetsDirectory}</snippets>
</attributes>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>2.0.5.BUILD-SNAPSHOT</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<!-- 自定义打包插件讲文档位置打包时放入固定位置 -->
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-resources</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.outputDirectory}/static/docs</outputDirectory>
<resources>
<resource>
<directory>${project.build.directory}/generated-docs</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

创建接口并且生成单元测试

  • 下面我写了两个简单的Rest 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
@PostMapping("/addUser")
public User addUser(User user) {


return user;
}


@PostMapping("/updateUser")
public User updateUser(@RequestBody User user) {

return user;
}
  • 为接口编写单元测试

为了方便我将很多方法import模块时时采用了import static的方式

在写单元测试的时候我们需要注意一点,我们需要填写接收请求体的和响应请求头的 header,比如下面
的accept(MediaType.APPLICATION_JSON)和contentType(MediaType.APPLICATION_JSON),不然在采用非
MediaType.TEXT_PLAIN 格式的请求体的时候会报错,然后我们关注一下
andDo(document ….这后面的代码,这部分代码就是编写 文档的代码,以下第一个测试接口为例,我们在documet方法内指定了addUser参数,这个参数代表我们会在之前在 maven plugin中配置的配置的
${project.build.directory}/generated-snippets 目录下生成了一个addUser文件夹,这文件夹下存放了asciidoc的文件
到这里你可能不太明白,到后面我们编译出文件后你就会明白了,requestHeaders是设置请求头,里面的方法headerWithName
设置了请求头说需要的参数,description为这个请求头的描述,后续spring rest doc会根据这些方法生成对应的表格文件,
requestParameters 方法设置了请求体的参数,parameterWithName方法设定了请求参数的具体值, responseFields方法可以理解为设置响应体,fieldWithPath
方法为设置响应体具体的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package com.test;


import static org.junit.Assert.*;

import com.alibaba.fastjson.JSON;
import org.junit.Before;
import org.junit.Rule;
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.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.headers.HeaderDocumentation.*;
import static org.springframework.restdocs.request.RequestDocumentation.*;


/**
* @author Liush
* @description
* @date 2020/8/19 21:08
**/
@RunWith(SpringRunner.class)
@SpringBootTest
public class Test {



@Autowired
private WebApplicationContext webApplicationContext;

@Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets");


private MockMvc mockMvc;


@Before
public void setUp(){

mockMvc=MockMvcBuilders.webAppContextSetup(webApplicationContext).apply(documentationConfiguration(restDocumentation)).build();

}

/**
* requestFields 可以理解为添加的示例数据入{"id":"222","name":"Liush"}
* responseFields 同理
* requestParameters 为入参数,使用此方法添加后会生成入场表格文件
*/

@org.junit.Test
public void addUserTest()throws Exception{
mockMvc.perform(post("/addUser").accept(MediaType.APPLICATION_JSON).param("id","1").
param("name","liush").header("token","token")
).andExpect(jsonPath("$.id",is("1"))).andExpect(jsonPath("$.name",is("liush"))).
andExpect(status().isOk()).andDo(document("addUser",
requestHeaders(headerWithName("token").description("校验token")),
requestParameters(parameterWithName("id").description("用户id"),
parameterWithName("name").description("用户名")
),
responseFields(fieldWithPath("id").description("用户id"),
fieldWithPath("name").description("用户名")

)
));

}


@org.junit.Test
public void updateUser() throws Exception {
User user=new User("222","Liush");
mockMvc.perform(post("/updateUser").accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON).content(JSON.toJSONString(user)))
.andExpect(jsonPath("$.id",is("222"))).andExpect(status().isOk()).
andDo(document("updateUser",
requestFields(fieldWithPath("id").description("用户id"),
fieldWithPath("name").description("用户名")

), responseFields(fieldWithPath("id").description("用户id"),fieldWithPath("name").description("用户名"))

))

;


}








}

看到这里你也许会有点懵,不过没关系,下面我们执行 mvn clean package命令查看具体目录结构后你就会一目了然vn
执行完该命令后,进入到target目录,下面会生成generated-snippets的目录,其有生成了一个addUser的目录
目录名字之前我们在document方法中指定了,其下又有9个文件,如果你对AsciiDoc有一些
了解你就会知道这些事AsciiDoc的文件

1
2
3
4
5
6
7
8
9
curl-request.adoc
http-request.adoc
http-response.adoc
httpie-request.adoc
request-body.adoc
request-headers.adoc
request-parameters.adoc
response-body.adoc
response-fields.adoc

打开request-parameters.adoc文件,发现其已经将我们的请求体参数自动生成了表格

1
2
3
4
5
6
7
8
9
10
|===
|Parameter|Description

|`+id+`
|用户id

|`+name+`
|用户名

|===

生成html文档

上面我们已经生成了AsciiDoc文件,现在我们就要将这些文件转换成html文档,首先我们需要在maven main
目录下新建一个asciidoc目录,里面新建一个index.adoc的文件,该文件名可以随意取,
我们编辑该文件,我们采用AsciiDoc语法写入一个文档,如果不熟悉AsciiDoc的语法可以
自行百度一下,很简单,我们重点关注
include命令,这里执行的就是将我们之前采用Spring rest doc生成的asccidoc文件引入到主文档来
我们再执行mvn clean package,我们就会再target目录下的 classes下的static下的docs下发现一个index.html
的html文档,然后我们访问该页面就能得到一个和Spring官方文档一样清爽的文档了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
= 接口文档
:author: liush
:email: 442549007@qq.com
:revnumber: v1.0
:toc: left
:toc-title: 接口文档

== 用户接口
=== 新增用户
*curl 示例*
include::{snippets}\addUser\curl-request.adoc[]
*请求头头*
include::{snippets}\addUser\request-headers.adoc[]
*入参*
include::{snippets}\addUser\request-parameters.adoc[]
*出参*
include::{snippets}\addUser\response-fields.adoc[]

=== 修改用户
*curl 示例*
include::{snippets}\updateUser\curl-request.adoc[]
*入参*
include::{snippets}\updateUser\request-fields.adoc[]
*入参示例*
include::{snippets}\updateUser\request-body.adoc[]
*出参*
include::{snippets}\updateUser\response-fields.adoc[]
*出参示例*
include::{snippets}\updateUser\response-body.adoc[]