Practice Springboot automatically generate REST API documentation

1. The question raised

  In a web project, often as necessary to provide REST API interface documentation for testing or secondary development. Hand-coded interface documentation work quite arduous, and now springboot project can be used in spring restdocs and swagger combined with two types of tools to generate custom style REST API documentation.
  swagger is a veteran restapi test tool, you can use it to generate test rest api page interface I believe that the use of most of the students are not familiar with it. spring restdocs may generate http requests and responses from the corresponding segment of the test in the test unit, and the final release of the document template document set (.adoc format) generated according to.
  Restdocs purpose is to allow binding and swagger swagger digest generated according to a user defined API (labeled) of API, the API and then filled into a final combined digest document according to the document restdocs http requests and responses generated fragments.
  Here we look at how specific operations.

2. pom configuration file

2.1 adding dependencies

  swagger dependencies:

    <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.5.0</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.5.0</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-staticdocs</artifactId>
            <version>2.6.1</version>
        </dependency>

  restdocs dependencies:

<dependency>
                <groupId>org.springframework.restdocs</groupId>
                <artifactId>spring-restdocs-mockmvc</artifactId>
                <version>${spring-restdocs.version}</version>
                <scope>test</scope>
            </dependency>

2.2 Plug join restdocs

  Note defines two attributes as environment variables in the plugin: generated is generated API swagger summary file directory.

<plugin>
                    <groupId>org.asciidoctor</groupId>
                    <artifactId>asciidoctor-maven-plugin</artifactId>
                    <version>${plugin-asciidoctor.version}</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>
                                    <generated>${project.build.directory}/swagger</generated>
                                </attributes>
                            </configuration>
                        </execution>
                    </executions>
                    <dependencies>
                        <dependency>
                            <groupId>org.springframework.restdocs</groupId>
                            <artifactId>spring-restdocs-asciidoctor</artifactId>
                            <version>${spring-restdocs.version}</version>
                        </dependency>
                    </dependencies>
                </plugin>

3. Rest API interfaces defined swagger

  Examples used herein swagger profile is defined as follows:

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket swaggerSpringMvcPlugin() {
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)).build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder().title("设备分组服务测试").description("单元测试restdocdemo")
                .contact(new Contact("智能运维产品部", "http://com.kedacom.com", "[email protected]")).version("1.0").build();
    }

}

  Controller examples used herein is defined as follows:

@Controller
@RequestMapping("/groupdevice/group")
@Api(value = "设备分组的REST接口")
public class GroupController {

    @Autowired
    private IGroupService groupService;

    @Data
    @AllArgsConstructor
    public static class ResultBody {
        //public ResultBody() {};
        private int errCode;
        private Object resultObj;
    }

    @RequestMapping(value = "/addGroup", method = RequestMethod.POST)
    @ResponseBody
    @ApiOperation(value="创建分组",notes = "创建一个新的分组")
    @ApiImplicitParam(name = "groupBody", value = "分组信息", required = true, paramType = "body",dataType = "Group")
    public ResultBody addGroup(@RequestBody Group groupBody) {
        if (groupService.save(groupBody)) {
            return new ResultBody(0, "add group ok.");
        } else {
            return new ResultBody(-1, "add group fail.");
        }
    }

    @RequestMapping(value = "/findGroupById", method = RequestMethod.GET)
    @ResponseBody
    @ApiOperation(value="查找分组",notes = "按分组ID查找分组")
    @ApiImplicitParam(name = "id", value = "分组ID", required = true, paramType = "query",dataType = "string")
    public ResultBody findGroup(@RequestParam(required = false) String id) {
        if (null != id) {
            Group group = groupService.getById(id);
            return new ResultBody(0, group);

        } else {
            return new ResultBody(-2, "parameter is invalid.");
        }
    }

    @RequestMapping(value = "/deleteGroupById", method = RequestMethod.DELETE)
    @ResponseBody
    @ApiOperation(value="删除分组",notes = "按分组ID删除分组")
    @ApiImplicitParam(name = "id", value = "分组ID", required = true, paramType = "query", dataType = "string")
    public ResultBody delGroup(@RequestParam(required = false) String id) {
        if (null != id) {
            if (groupService.removeById(id)) {
                return new ResultBody(0, "delete group ok.");
            } else {
                return new ResultBody(-1, "delete group fail.");
            }
        } else {
            return new ResultBody(-2, "parameter is invalid.");
        }
    }

}

  The Controller API defines three interfaces.

4. Writing Unit Tests

   The previous section defined interface, can be provided by using MockMvc restdocs unit test and generate http requests and responses corresponding clip. Unit test code is as follows:

package com.kedacom.ioms.groupdevice.controller;

import static org.junit.Assert.*;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.lang.reflect.Method;

import org.junit.*;
import org.junit.runner.RunWith;
import org.junit.FixMethodOrder;
import org.junit.runners.MethodSorters;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.bind.annotation.RequestMethod;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.kedacom.ioms.groupdevice.entity.Group;
import com.kedacom.ioms.groupdevice.service.IGroupService;

import io.github.robwin.markup.builder.MarkupLanguage;
import io.github.robwin.swagger2markup.GroupBy;
import io.github.robwin.swagger2markup.Swagger2MarkupConverter;
import io.swagger.annotations.ApiOperation;
import springfox.documentation.staticdocs.SwaggerResultHandler;

/**
 * @author zhang.kai
 *
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@AutoConfigureMockMvc
@AutoConfigureRestDocs(outputDir = "target/snippets")
public class GroupControllerTest {

    // swagger生成的adoc文件存放目录
    private static final String swaggerOutputDir = "target/swagger";
    // http请求回应生成的snippets文件存放目录
    private static final String snippetsOutputDir = "target/snippets";

    @Autowired
    private IGroupService groupService;

    @Autowired
    private MockMvc mvc;

    /**
     * 断言rest结果是否正确,根据response结果中的errorcode是否为0来判断
     * 
     * @param response
     * @throws Exception
     */
    @SuppressWarnings("unused")
    private void assertResultOk(MockHttpServletResponse response) throws Exception {
        assertNotNull(response);
        String retstr = response.getContentAsString();
        GroupController.ResultBody rb = JSON.parseObject(retstr, GroupController.ResultBody.class);
        assertNotNull(rb);
        assertEquals(0, rb.getErrCode());

    }

    /**
     * 返回controller中api方法的@ApiOperation注解中的value值
     * 
     * @param classname
     * @param methodname
     * @return
     * @throws Exception
     */
    private String getApiOperValue(String classname, String methodname, Class<?>... parameterTypes) throws Exception {
        Class classApi = Class.forName(classname);
        Method method = classApi.getMethod(methodname, parameterTypes);
        ApiOperation apiOper = method.getAnnotation(ApiOperation.class);
        return apiOper.value();
    }

    /**
     * 执行rest操作
     * 
     * @param requestUrl
     * @param method
     * @param            docOutDir: 生成请求消息体存放的文件目录,本示例中使用ApiOperation的value作为目录名
     * @param            requestBody:请求的消息体
     * @throws Exception
     */
    private MockHttpServletResponse performRestRequest(String requestUrl, RequestMethod method, String docOutDir,
            Object... requestBody) throws Exception {
        MvcResult mvcResult;
        MockHttpServletResponse response;

        switch (method) {
        case GET:
            mvcResult = mvc.perform(MockMvcRequestBuilders.get(requestUrl).accept(MediaType.APPLICATION_JSON_UTF8))
                    .andDo(print())
                    .andDo(document(docOutDir, preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
                    .andReturn();
            break;
        case POST:
            mvcResult = mvc
                    .perform(MockMvcRequestBuilders.post(requestUrl).contentType(MediaType.APPLICATION_JSON_UTF8)
                            .content(JSON.toJSONString(requestBody[0])).accept(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andDo(document(docOutDir, preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
                    .andReturn();
            break;
        case PUT:
            mvcResult = mvc
                    .perform(MockMvcRequestBuilders.put(requestUrl).contentType(MediaType.APPLICATION_JSON_UTF8)
                            .content(JSON.toJSONString(requestBody[0])).accept(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andDo(document(docOutDir, preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
                    .andReturn();
            break;
        case DELETE:
            mvcResult = mvc.perform(MockMvcRequestBuilders.delete(requestUrl).accept(MediaType.APPLICATION_JSON_UTF8))
                    .andDo(print())
                    .andDo(document(docOutDir, preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
                    .andReturn();
            break;
        default:
            return null;
        }
        response = mvcResult.getResponse();
        return response;
    }

    @Test
    public void test01AddGroup() throws Exception {
        Group groupIns = new Group();
        groupIns.setVid(1l);
        groupIns.setSyncRoundNum(1);
        groupIns.setTreeId("00001");
        groupIns.setIdParent("parentid1");
        groupIns.setIdParentOrginal("idParentOrginal1");
        groupIns.setId("id1");
        groupIns.setIdOrginal("idorginal1");
        groupIns.setLeft(1);
        groupIns.setRight(100);
        groupIns.setLevel(4);
        groupIns.setRank(1);
        groupIns.setGbId("4200000123400000000");
        groupIns.setGbIdOrginal("4200000123400000000");
        groupIns.setName("group1");

        String requestUrl = "/groupdevice/group/" + "addGroup";
        String docOutDir = getApiOperValue("com.kedacom.ioms.groupdevice.controller.GroupController", "addGroup",
                Group.class);
        MockHttpServletResponse response = performRestRequest(requestUrl, RequestMethod.POST, docOutDir, groupIns);
        assertResultOk(response);
    }

    @Test
    public void test02FindGroup() throws Exception {
        // 取最大vid记录查询之
        Group group1 = groupService.getOne(new QueryWrapper<Group>().select("vid").orderByDesc("vid").last("limit 1"));
        assertNotNull(group1);
        String requestUrl = "/groupdevice/group/" + "findGroupById" + "?id=" + group1.getVid();
        String docOutDir = getApiOperValue("com.kedacom.ioms.groupdevice.controller.GroupController", "findGroup",
                String.class);
        MockHttpServletResponse response = performRestRequest(requestUrl, RequestMethod.GET, docOutDir);
        assertResultOk(response);
    }

    @Test
    public void test03DelGroup() throws Exception {
        // 取最大vid记录删除之
        Group group1 = groupService.getOne(new QueryWrapper<Group>().select("vid").orderByDesc("vid").last("limit 1"));
        assertNotNull(group1);
        String requestUrl = "/groupdevice/group/" + "deleteGroupById" + "?id=" + group1.getVid();
        String docOutDir = getApiOperValue("com.kedacom.ioms.groupdevice.controller.GroupController", "delGroup",
                String.class);
        MockHttpServletResponse response = performRestRequest(requestUrl, RequestMethod.DELETE, docOutDir);
        assertResultOk(response);
    }

    /**
     * 这是一个特殊的测试用例,目的仅为生成一个swagger.json的文件
     * @throws Exception
     */
    @Test
    public void genSwaggerFile() throws Exception {
        // 得到swagger.json,写入outputDir目录中
        mvc.perform(MockMvcRequestBuilders.get("/v2/api-docs").accept(MediaType.APPLICATION_JSON))
                .andDo(SwaggerResultHandler.outputDirectory(swaggerOutputDir).build()).andExpect(status().isOk())
                .andReturn();
    }
    
    /**
                 * 所有的测试用例执行完之后执行该函数,根据生成的swagger.json和snippets生成对应的adoc文档
     * @throws Exception
     */
    @AfterClass
    public static void outputSwaggerDoc() throws Exception {

        // 读取上一步生成的swagger.json转成asciiDoc,写入到outputDir
        // 这个outputDir必须和插件里面<generated></generated>标签配置一致
        Swagger2MarkupConverter.from(swaggerOutputDir + "/swagger.json").withPathsGroupedBy(GroupBy.TAGS)// 按tag排序
                .withMarkupLanguage(MarkupLanguage.ASCIIDOC)// 格式
                .withExamples(snippetsOutputDir).build().intoFolder(swaggerOutputDir);// 输出
    }
}

  In this test unit, MockMvc API call and the resulting http request in response to the output fragment snippetsOutputDir directory (performRestRequest function), snippetsOutputDir directory with a directory for each API fragments are values ​​swagger value of annotation @ApiOperation ( getApiOperValue function). The testing unit generates the file and directory as shown below:


13196023-a99cc2baf5a37ed9.png
Figure 1.png

   Interface Summary To make use of these swagger http request response segment, swagger need to generate API interface according digest document annotation. GenSwaggerFile generating function according swagger swagger.json annotation file, the file is as follows:

{
    "swagger": "2.0",
    "info": {
        "description": "单元测试restdocdemo",
        "version": "1.0",
        "title": "设备分组服务测试",
        "contact": {
            "name": "智能运维产品部",
            "url": "http://com.kedacom.com",
            "email": "[email protected]"
        }
    },
    "host": "localhost:8080",
    "basePath": "/",
    "tags": [
        {
            "name": "group-controller",
            "description": "Group Controller"
        }
    ],
    "paths": {
        "/groupdevice/group/addGroup": {
            "post": {
                "tags": [
                    "group-controller"
                ],
                "summary": "创建分组",
                "description": "创建一个新的分组",
                "operationId": "addGroupUsingPOST",
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "*/*"
                ],
                "parameters": [
                    {
                        "in": "body",
                        "name": "groupBody",
                        "description": "分组信息",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "schema": {
                            "$ref": "#/definitions/ResultBody"
                        }
                    },
                    "201": {
                        "description": "Created"
                    },
                    "401": {
                        "description": "Unauthorized"
                    },
                    "403": {
                        "description": "Forbidden"
                    },
                    "404": {
                        "description": "Not Found"
                    }
                }
            }
        },
        "/groupdevice/group/deleteGroupById": {
            "delete": {
                "tags": [
                    "group-controller"
                ],
                "summary": "删除分组",
                "description": "按分组ID删除分组",
                "operationId": "delGroupUsingDELETE",
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "*/*"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "query",
                        "description": "分组ID",
                        "required": true,
                        "type": "string"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "schema": {
                            "$ref": "#/definitions/ResultBody"
                        }
                    },
                    "401": {
                        "description": "Unauthorized"
                    },
                    "204": {
                        "description": "No Content"
                    },
                    "403": {
                        "description": "Forbidden"
                    }
                }
            }
        },
        "/groupdevice/group/findGroupById": {
            "get": {
                "tags": [
                    "group-controller"
                ],
                "summary": "查找分组",
                "description": "按分组ID查找分组",
                "operationId": "findGroupUsingGET",
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "*/*"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "query",
                        "description": "分组ID",
                        "required": true,
                        "type": "string"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "schema": {
                            "$ref": "#/definitions/ResultBody"
                        }
                    },
                    "401": {
                        "description": "Unauthorized"
                    },
                    "403": {
                        "description": "Forbidden"
                    },
                    "404": {
                        "description": "Not Found"
                    }
                }
            }
        }
    },
    "definitions": {
        "Group": {
            "type": "object",
            "properties": {
                "gbId": {
                    "type": "string"
                },
                "gbIdOrginal": {
                    "type": "string"
                },
                "id": {
                    "type": "string"
                },
                "idOrginal": {
                    "type": "string"
                },
                "idParent": {
                    "type": "string"
                },
                "idParentOrginal": {
                    "type": "string"
                },
                "left": {
                    "type": "integer",
                    "format": "int32"
                },
                "level": {
                    "type": "integer",
                    "format": "int32"
                },
                "name": {
                    "type": "string"
                },
                "rank": {
                    "type": "integer",
                    "format": "int32"
                },
                "right": {
                    "type": "integer",
                    "format": "int32"
                },
                "syncRoundNum": {
                    "type": "integer",
                    "format": "int32"
                },
                "treeId": {
                    "type": "string"
                },
                "vid": {
                    "type": "integer",
                    "format": "int64"
                }
            }
        },
        "ResultBody": {
            "type": "object",
            "properties": {
                "errCode": {
                    "type": "integer",
                    "format": "int32"
                },
                "resultObj": {
                    "type": "object"
                }
            }
        }
    }
}

  According to the above function outputSwaggerDoc swagger.json file and directory binding http request response segment of FIG. 1 interface description file generating rest api, where three files are generated, as shown below:


13196023-5a6d3dc5935f34e5.png
Figure 2.png

  Note that the detailed information on each interface file generated paths.adoc described, and in each of the summary file attribute swagger.json URL each interface as a title, and this title http request also in FIG. 1 clip file directory corresponding to the response. As shown below:

13196023-b2faf259833443af.png
Figure 3.png

  Now all segments documents have been generated, the need to integrate into a single document. restdocs default template document storage directory is src / main / asciidoc, as shown below:


13196023-bfbed5324a39381c.png
Figure 4.png

  This is our index.adoc preset, long like this. Note that references herein "{generated}" environment variable is set in the file swagger 2.2 interfaces generated document fragments.

include::{generated}/overview.adoc[]
include::{generated}/definitions.adoc[]
include::{generated}/paths.adoc[]

5. maven constructed to produce the final document

  Use "mvn package" command works can be constructed and packaged, but also to produce the final document (an html file format defined pom), which generates a default directory file as shown below:


13196023-8f1ec2b9e3e498c1.png
Figure 5.png

  index.html final presentation of results as shown below:


13196023-78d5da746f36642a.png
Figure 6.png

6. Summary

  swagger API interface can be generated using the annotation information, restdocs example of a message in response http request unit generating a corresponding test interface, bringing both interface document can be generated containing both interface description further comprises a request response message example. restdocs generated here http request response message to the directory using the value acquired @ApiOperation to define, with the document to generate corresponding header swagger for message fragments corresponding to injection.

references:

  1. https://www.jianshu.com/p/af7a6f29bf4f
  2. https://blog.csdn.net/luxinghong199106/article/details/86683158

Guess you like

Origin blog.csdn.net/weixin_34061042/article/details/90895459