Springboot 系列(十六)你真的瞭解 Swagger 文檔嗎?

前言

目前來講,在 Java 領域使用 Springboot 構建微服務是比較流行的,在構建微服務時,咱們大多數會選擇暴漏一個 REST API 以供調用。又或者公司採用先後端分離的開發模式,讓前端和後端的工做由徹底不一樣的工程師進行開發完成。不論是微服務仍是這種先後端分離開發,維持一份完整的及時更新的 REST API 文檔,會極大的提升咱們的工做效率。而傳統的文檔更新方式(如手動編寫),很難保證文檔的及時性,常常會年久失修,失去應有的意義。所以選擇一種新的 API 文檔維護方式頗有必要,這也是這篇文章要介紹的內容。
html

1. OpenAPI 規範介紹

OpenAPI Specification 簡稱 OAS,中文也稱 OpenAPI 描述規範,使用 OpenAPI 文件能夠描述整個 API,它制定了一套的適合通用的與語言無關的 REST API 描述規範,如 API 路徑規範、請求方法規範、請求參數規範、返回格式規範等各類相關信息,令人類和計算機均可以不須要訪問源代碼就能夠理解和使用服務的功能。前端

下面是 OpenAPI 規範中建議的 API 設計規範,基本路徑設計規範。java

https://api.example.com/v1/users?role=admin&status=active
\________________________/\____/ \______________________/
         server URL       endpoint    query parameters
                            path

對於傳參的設計也有規範,能夠像下面這樣:git

OpenAPI 規範的東西遠遠不止這些,目前 OpenAPI 規範最新版本是 3.0.2,若是你想了解更多的 OpenAPI 規範,能夠訪問下面的連接。
OpenAPI Specification (https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md)github

2. Swagger 介紹

不少人都覺得 Swagger 只是一個接口文檔生成框架,其實並非。 Swagger 是一個圍繞着 OpenAPI Specification(OAS,中文也稱 OpenAPI規範)構建的一組開源工具。能夠幫助你從 API 的設計到 API 文檔的輸出再到 API 的測試,直至最後的 API 部署等整個 API 的開發週期提供相應的解決方案,是一個龐大的項目。 Swagger 不只免費,並且開源,無論你是企業用戶仍是我的玩家,均可以使用 Swagger 提供的方案構建使人驚豔的 REST APIweb

Swagger 有幾個主要的產品。面試

  • Swagger Editor – 一個基於瀏覽器的 Open API 規範編輯器。
  • Swagger UI – 一個將 OpenAPI 規範呈現爲可交互在線文檔的工具。
  • Swagger Codegen – 一個根據 OpenAPI 生成調用代碼的工具。

若是你想了解更多信息,能夠訪問 Swagger 官方網站 https://swagger.iospring

3. Springfox 介紹

源於 Java 中 Spring 框架的流行,讓一個叫作 Marrty Pitt 的老外有了爲 SpringMVC 添加接口描述的想法,所以他建立了一個遵照 OpenAPI 規範(OAS)的項目,取名爲 swagger-springmvc,這個項目可讓 Spring 項目自動生成 JSON 格式的 OpenAPI 文檔。這個框架也仿照了 Spring 項目的開發習慣,使用註解來進行信息配置。shell

後來這個項目發展成爲 Springfox,再後來擴展出 springfox-swagger2 ,爲了讓 JSON 格式的 API 文檔更好的呈現,又出現了 springfox-swagger-ui 用來展現和測試生成的 OpenAPI 。這裏的 springfox-swagger-ui 其實就是上面介紹的 Swagger-ui,只是它被經過 webjar 的方式打包到 jar 包內,並經過 maven 的方式引入進來。json

上面提到了 Springfox-swagger2 也是經過註解進行信息配置的,那麼是怎麼使用的呢?下面列舉經常使用的一些註解,這些註解在下面的 Springboot 整合 Swagger 中會用到。

註解 示例 描述
@ApiModel @ApiModel(value = "用戶對象") 描述一個實體對象
@ApiModelProperty @ApiModelProperty(value = "用戶ID", required = true, example = "1000") 描述屬性信息,執行描述,是否必須,給出示例
@Api @Api(value = "用戶操做 API(v1)", tags = "用戶操做接口") 用在接口類上,爲接口類添加描述
@ApiOperation @ApiOperation(value = "新增用戶") 描述類的一個方法或者說一個接口
@ApiParam @ApiParam(value = "用戶名", required = true) 描述單個參數

更多的 Springfox 介紹,能夠訪問 Springfox 官方網站。

Springfox Reference Documentation (http://springfox.github.io)

4. Springboot 整合 Swagger

就目前來講 ,Springboot 框架是很是流行的微服務框架,在微服務框架下,不少時候咱們都是直接提供 REST API 的。REST API 若是沒有文檔的話,使用者就很頭疼了。不過不用擔憂,上面說了有一位叫 Marrty Pitt 的老外已經建立了一個發展成爲 Springfox 的項目,能夠方便的提供 JSON 格式的 OpenAPI 規範和文檔支持。且擴展出了 springfox-swagger-ui 用於頁面的展現。

須要注意的是,這裏使用的所謂的 Swagger 其實和真正的 Swagger 並非一個東西,這裏使用的是 Springfox 提供的 Swagger 實現。它們都是基於 OpenAPI 規範進行 API 構建。因此也均可以 Swagger-ui 進行 API 的頁面呈現。

4.1. 建立項目

如何建立一個 Springboot 項目這裏不提,你能夠直接從 Springboot 官方 下載一個標準項目,也可使用 idea 快速建立一個 Springboot 項目,也能夠順便拷貝一個 Springboot 項目過來測試,總之,方式多種多樣,任你選擇。

下面演示如何在 Springboot 項目中使用 swagger2。

4.2. 引入依賴

這裏主要是引入了 springfox-swagger2,能夠經過註解生成 JSON 格式的 OpenAPI 接口文檔,而後因爲 Springfox 須要依賴 jackson,因此引入之。springfox-swagger-ui 能夠把生成的 OpenAPI 接口文檔顯示爲頁面。Lombok 的引入能夠經過註解爲實體類生成 get/set 方法。

<dependencies> 
    <!-- Spring Boot web 開發整合 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <artifactId>spring-boot-starter-json</artifactId>
                <groupId>org.springframework.boot</groupId>
            </exclusion>
        </exclusions>
    </dependency>

    <!-- 引入swagger2的依賴-->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    
    <!-- jackson相關依賴 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.5.4</version>
    </dependency>

    <!-- Lombok 工具 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

4.3. 配置 Springfox-swagger

Springfox-swagger 的配置經過一個 Docket 來包裝,Docket 裏的 apiInfo 方法能夠傳入關於接口整體的描述信息。而 apis 方法能夠指定要掃描的包的具體路徑。在類上添加 @Configuration 聲明這是一個配置類,最後使用 @EnableSwagger2 開啓 Springfox-swagger2。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * <p>
 * Springfox-swagger2 配置
 *
 * @Author niujinpeng
 * @Date 2019/11/19 23:17
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("net.codingme.boot.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("未讀代碼 API")
                .description("公衆號:未讀代碼(weidudaima) springboot-swagger2 在線藉口文檔")
                .termsOfServiceUrl("https://www.codingme.net")
                .contact("達西呀")
                .version("1.0")
                .build();
    }
}

4.4. 代碼編寫

文章不會把全部代碼一一列出來,這沒有太大意義,因此只貼出主要代碼,完整代碼會上傳到 Github,並在文章底部附上 Github 連接。

參數實體類 User.java,使用 @ApiModel@ApiModelProperty 描述參數對象,使用 @NotNull 進行數據校驗,使用 @Data 爲參數實體類自動生成 get/set 方法。

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;

import javax.validation.constraints.NotNull;
import java.util.Date;

/**
 * <p>
 * 用戶實體類
 *
 * @Author niujinpeng
 * @Date 2018/12/19 17:13
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "用戶對象")
public class User {

    /**
     * 用戶ID
     *
     * @Id 主鍵
     * @GeneratedValue 自增主鍵
     */
    @NotNull(message = "用戶 ID 不能爲空")
    @ApiModelProperty(value = "用戶ID", required = true, example = "1000")
    private Integer id;

    /**
     * 用戶名
     */
    @NotNull(message = "用戶名不能爲空")
    @ApiModelProperty(value = "用戶名", required = true)
    private String username;
    /**
     * 密碼
     */
    @NotNull(message = "密碼不能爲空")
    @ApiModelProperty(value = "用戶密碼", required = true)
    private String password;
    /**
     * 年齡
     */
    @ApiModelProperty(value = "用戶年齡", example = "18")
    private Integer age;
    /**
     * 生日
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd hh:mm:ss")
    @ApiModelProperty(value = "用戶生日")
    private Date birthday;
    /**
     * 技能
     */
    @ApiModelProperty(value = "用戶技能")
    private String skills;
}

編寫 Controller 層,使用 @Api 描述接口類,使用 @ApiOperation 描述接口,使用 @ApiParam 描述接口參數。代碼中在查詢用戶信息的兩個接口上都添加了 tags = "用戶查詢" 標記,這樣這兩個方法在生成 Swagger 接口文檔時候會分到一個共同的標籤組裏。

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import net.codingme.boot.domain.Response;
import net.codingme.boot.domain.User;
import net.codingme.boot.enums.ResponseEnum;
import net.codingme.boot.utils.ResponseUtill;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;

/**
 * <p>
 * 用戶操做
 *
 * @Author niujinpeng
 * @Date 2019/11/19 23:17
 */

@Slf4j
@RestController(value = "/v1")
@Api(value = "用戶操做 API(v1)", tags = "用戶操做接口")
public class UserController {

    @ApiOperation(value = "新增用戶")
    @PostMapping(value = "/user")
    public Response create(@Valid User user, BindingResult bindingResult) throws Exception {
        if (bindingResult.hasErrors()) {
            String message = bindingResult.getFieldError().getDefaultMessage();
            log.info(message);
            return ResponseUtill.error(ResponseEnum.ERROR.getCode(), message);
        } else {
            // 新增用戶信息 do something
            return ResponseUtill.success("用戶[" + user.getUsername() + "]信息已新增");
        }
    }

    @ApiOperation(value = "刪除用戶")
    @DeleteMapping(value = "/user/{username}")
    public Response delete(@PathVariable("username")
                           @ApiParam(value = "用戶名", required = true) String name) throws Exception {
        // 刪除用戶信息 do something
        return ResponseUtill.success("用戶[" + name + "]信息已刪除");
    }

    @ApiOperation(value = "修改用戶")
    @PutMapping(value = "/user")
    public Response update(@Valid User user, BindingResult bindingResult) throws Exception {
        if (bindingResult.hasErrors()) {
            String message = bindingResult.getFieldError().getDefaultMessage();
            log.info(message);
            return ResponseUtill.error(ResponseEnum.ERROR.getCode(), message);
        } else {
            String username = user.getUsername();
            return ResponseUtill.success("用戶[" + username + "]信息已修改");
        }
    }

    @ApiOperation(value = "獲取單個用戶信息", tags = "用戶查詢")
    @GetMapping(value = "/user/{username}")
    public Response get(@PathVariable("username")
                        @NotNull(message = "用戶名稱不能爲空")
                        @ApiParam(value = "用戶名", required = true) String username) throws Exception {
        // 查詢用戶信息 do something
        User user = new User();
        user.setId(10000);
        user.setUsername(username);
        user.setAge(99);
        user.setSkills("cnp");
        return ResponseUtill.success(user);
    }

    @ApiOperation(value = "獲取用戶列表", tags = "用戶查詢")
    @GetMapping(value = "/user")
    public Response selectAll() throws Exception {
        // 查詢用戶信息列表 do something
        User user = new User();
        user.setId(10000);
        user.setUsername("未讀代碼");
        user.setAge(99);
        user.setSkills("cnp");
        List<User> userList = new ArrayList<>();
        userList.add(user);
        return ResponseUtill.success(userList);
    }
}

最後,爲了讓代碼變得更加符合規範和好用,使用一個統一的類進行接口響應。

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "響應信息")
public class Response {
    /**
     * 響應碼
     */
    @ApiModelProperty(value = "響應碼")
    private String code;
    /**
     * 響應信息
     */
    @ApiModelProperty(value = "響應信息")
    private String message;

    /**
     * 響應數據
     */
    @ApiModelProperty(value = "響應數據")
    private Collection content;
}

4.5. 運行訪問

直接啓動 Springboog 項目,能夠看到控制檯輸出掃描到的各個接口的訪問路徑,其中就有 /2/api-docs

這個也就是生成的 OpenAPI 規範的描述 JSON 訪問路徑,訪問能夠看到。

由於上面咱們在引入依賴時,也引入了 springfox-swagger-ui 包,因此還能夠訪問 API 的頁面文檔。訪問路徑是 /swagger-ui.html,訪問看到的效果能夠看下圖。

也能夠看到用戶查詢的兩個方法會歸到了一塊兒,緣由就是這兩個方法的註解上使用相同的 tag 屬性。

4.7. 調用測試

springfox-swagger-ui 不只是生成了 API 文檔,還提供了調用測試功能。下面是在頁面上測試獲取單個用戶信息的過程。

  1. 點擊接口 [/user/{username}] 獲取單個用戶信息。
  2. 點擊 **Try it out** 進入測試傳參頁面。
  3. 輸入參數,點擊 Execute 藍色按鈕執行調用。
  4. 查看返回信息。

下面是測試時的響應截圖。

5. 常見報錯

若是你在程序運行中常常發現像下面這樣的報錯。

java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) ~[na:1.8.0_111]
    at java.lang.Long.parseLong(Long.java:601) ~[na:1.8.0_111]
    at java.lang.Long.valueOf(Long.java:803) ~[na:1.8.0_111]
    at io.swagger.models.parameters.AbstractSerializableParameter.getExample(AbstractSerializableParameter.java:412) ~[swagger-models-1.5.20.jar:1.5.20]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_111]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_111]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_111]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_111]
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:536) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:666) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:156) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:113) [jackson-databind-2.5.4.jar:2.5.4]

那麼你須要檢查使用了 @ApiModelProperty 註解且字段類型爲數字類型的屬性上,@ApiModelProperty 註解是否設置了 example 值,若是沒有,那就須要設置一下,像下面這樣。

@NotNull(message = "用戶 ID 不能爲空")
@ApiModelProperty(value = "用戶ID", required = true, example = "1000")
private Integer id;

文中代碼都已經上傳到 https://github.com/niumoo/springboot

參考文檔

<完>
我的網站: https://www.codingme.net
若是你喜歡這篇文章,能夠關注公衆號,一塊兒成長。
關注公衆號回覆資源能夠沒有套路的獲取全網最火的的 Java 核心知識整理&面試核心資料。
公衆號

相關文章
相關標籤/搜索