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

  • 2019 年 12 月 5 日
  • 筆記

前言

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

1. OpenAPI 規範介紹

Open API

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

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

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

對於傳參的設計也有規範,可以像下面這樣:

  • 路徑參數, 例如 /users/{id}
  • 查詢參數, 例如 /users?role=未讀代碼
  • header 參數, 例如 X-MyHeader: Value
  • cookie 參數, 例如 Cookie: debug=0; csrftoken=BUSe35dohU3O1MZvDCU

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

2. Swagger 介紹

swagger

很多人都以為 Swagger 只是一個接口文檔生成框架,其實並不是。Swagger 是一個圍繞着 OpenAPI Specification(OAS,中文也稱 OpenAPI規範)構建的一組開源工具。可以幫助你從 API 的設計到 API 文檔的輸出再到 API 的測試,直至最後的 API 部署等整個 API 的開發周期提供相應的解決方案,是一個龐大的項目。Swagger 不僅免費,而且開源,不管你是企業用戶還是個人玩家,都可以使用 Swagger 提供的方案構建令人驚艷的 REST API

Swagger 有幾個主要的產品。

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

如果你想了解更多信息,可以訪問 Swagger 官方網站 https://swagger.io。

3. Springfox 介紹

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

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

上面提到了 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

Springboot 啟動

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

OpenAPI – JSON

因為上面我們在引入依賴時,也引入了 springfox-swagger-ui 包,所以還可以訪問 API 的頁面文檔。訪問路徑是 /swagger-ui.html,訪問看到的效果可以看下圖。

swagger 訪問

也可以看到用戶查詢的兩個方法會歸到了一起,原因就是這兩個方法的註解上使用相同的 tag 屬性。

4.7. 調用測試

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

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

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

swagger 測試

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

參考文檔

  • OpenAPI Specification
  • Swagger Documentation
  • Springfox Reference Documentation