天天看點

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

前言

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

1. OpenAPI 規範介紹

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

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 介紹

Springboot 系列(一)你真的了解 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 系列(一)你真的了解 Swagger 文檔嗎?

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

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

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

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

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

4.7. 調用測試

springfox-swagger-ui 不僅是生成了 API 文檔,還提供了調用測試功能。下面是在頁面上測試擷取單個使用者資訊的過程。

  1. 點選接口 [/user/{username}] 擷取單個使用者資訊。
  2. 點選 Try it out 進入測試傳參頁面。
  3. 輸入參數,點選 Execute 藍色按鈕執行調用。
  4. 檢視傳回資訊。

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

Springboot 系列(一)你真的了解 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

繼續閱讀