springboot+Swagger2最佳實踐和使用規範

springboot+Swagger2最佳實踐和使用規範

1. 前言

本文主要的內容是:
1、springboot整合Swagger2,如需要看這部分內容可以直接跳到對應章節
2、討論swagger的使用規範,以及一些最佳實踐。
認真看完,你會有收穫的

swagger版本是:2.9.2(不同版本UI界面有可能不同)

swagger2和1,因爲2的版本可能對比1升級比較大,所以叫2,其實還是swagger

2. 使用規範

2.1 準備

先準備基礎的知識。

傳參一般使用兩種方式

  • 鍵值對

  • 傳JSON

鍵值對準確說是 “名值對”,即Content-Type是x-www-form-urlencoded或者form-data的傳參方式。

傳JSON 的 Content-Type是application/json。注意區分 “傳JSON字符串”,兩者在請求body的內容不一樣

// 傳JSON字符串,body內容如下
jsonStr={"name":"Stone"}

// 傳JSON,如下,沒有key的
{"name":"Stone"}

2.2 Swagger使用規範

只用@Api、@ApiOperation、@ApiParam、@ApiModelProperty就夠了

  • 禁止不指定請求方法,要指定用GET/POST…,用@RequestMapping不指定的時候會產生大量的垃圾接口
    在這裏插入圖片描述

在如下地方使用swagger的註解要注意

  • 控制器

    必須註解 @Api(tags = "..."),必須有tags

    @RestController
    @Api(tags = "用戶模塊")
    public class UserController {...}
    

    不想暴露控制器裏所有方法,用hiden屬性,要隱藏單個接口,在@ApiOperation裏使用hidden

  • 方法

    用 @ApiOperation,用value說明接口用途,notes用於更詳細的說明。value必出現,notes可選,沒有notes時value要省略以保證簡潔

    // 要省略value,保證簡潔,別 @ApiOperation(value = "獲取用戶信息")
    @ApiOperation("獲取用戶信息")
    @GetMapping("/user/get")
    public ResponseDTO<UserDO> getUser(
    
    // 當有更長的內容補充時,使用notes
    @ApiOperation(value = "獲取用戶列表", notes = "如果需要詳細的補充描述,在這裏寫,在value裏會讓標題很長")
    @GetMapping("/user/list")
    public ResponseDTO<List<UserDO>> listUser(
    
  • 方法入參

    • @ApiParam(value = “…”, required = true|false)

      任何控制器參數必須有這個註解(除HttpServletRequest…外)

      value和required任何情況都不能少(不論required是true是false)

    • @RequestParam(required = true|false)

      若使用該註解,required在任何情況都不可省略

      • 如果是單一類型,使用該註解(非單一類型例如類裏有多個字段組成,單一類型就是僅一個類型)
      • 如果是類的類型,無論傳鍵值對還是傳JSON,都不能加上(否則swagger不能展類裏的多字段)

    再次強調,required任何情況都不要省略,省略後不直觀,參數再長也不過一行

    // 單一類型,要用 @RequestParam(required)
    @ApiOperation("獲取用戶信息")
    @GetMapping("/user/get")
    public ResponseDTO<UserDO> getUser(
    	@ApiParam(value = "用戶ID", required = true) @RequestParam(required = true) Integer id
    )
    
    // 入參是一個類,禁止用 @RequestParam(required),這個例子是傳鍵值對的
    @ApiOperation("新增評論(鍵值對傳參方式)")
    @PostMapping("/comment/operate")
    public ResponseDTO<Integer> operateComment(
    	@ApiParam(value = "評論信息", required = true) @Valid CommentOperReqDTO commentOperReqDTO
    )
    
    // 入參是個類,禁止用 @RequestParam(required),這個例子是傳JSON的
    @ApiOperation("新增訂單")
    @PostMapping("/order/add")
    public ResponseDTO<OrderRespDTO> addOrder(
    	@ApiParam(value = "訂單信息", required = true) @Valid @RequestBody OrderReqDTO orderReqDTO
    )
    
  • 方法入參DTO

    • 只有類的類型才需要標註

    • DTO類上不需要任何註解,DTO所有字段都需要 @ApiModelProperty

      • 必須指定value和required,任何情況不得省略required,指定required是希望前端同學知道是否必填
      • 某個字段,比如ID,在新增時用不上可不填但是在修改時必填,這種情況讓required=false,並且在value中指出什麼情況下必填什麼時候可選
      • 內嵌其他類的字段也必須使用註解
      • 不要用example,例如 @ApiModelProperty(example = "ID", required = true)
      • 因爲入參需要校驗參數,有可能還會帶jsr303的註解
    // 類上不用註解
    // 沒個字段都必須用 @ApiModelProperty
    // 內嵌的類 contactInfo 和集合 prodInfoList 也必須用 @ApiModelProperty
    // @ApiModelProperty 必須包含 value 和 required
    // 內嵌的類用靜態內部類
    @Data
    public class OrderReq2DTO {
    
    
        @NotNull
        @ApiModelProperty(value = "商品總價", required = true)
        private Long totalPriceInCent;
    
        @ApiModelProperty(value = "備註(如果總價大於1萬,必須備註)", required = false)
        private String memo;
    
        @NotNull
        @Valid
        @ApiModelProperty(value = "聯繫信息", required = true)
        private ContactInfo contactInfo;
        
        @NotEmpty
        @Valid
        @ApiModelProperty(value = "商品信息列表", required = true)
        private List<ProdInfo> prodInfoList;
    
        
        @Data
        public static class ContactInfo {
            @NotBlank
            @ApiModelProperty(value = "郵寄地址", required = true)
            private String address;
            
            @NotBlank
            @ApiModelProperty(value = "手機", required = true)
            private String mobile;
        }
    
        @Data
        public static class ProdInfo {
            @ApiModelProperty(value = "商品ID", required = true)
            @NotBlank
            private String prodId;
    
            @ApiModelProperty(value = "商品單價", required = true)
            @NotNull
            @Min(0)
            private Long unitPriceInCent;
        }
    }
    
  • 方法出參DTO

    • 沒DTO的無需標記

    • 有DTO的 ,在DTO裏用 @ApiModelProperty("…"),禁止用required屬性

    • 由於響應參數無需校驗參數,所以不帶jsr303註解

      // 類上不需要標記
      // 每個字段都必須標記,包括嵌入字段,如這裏的 prodInfoList
      // 用 @ApiModelProperty("..."),不要用required
      @Data
      public class OrderRespDTO {
          @ApiModelProperty("生成的訂單編號")
          private String orderNo;
      
          @ApiModelProperty("訂單總價")
          private Long priceInCent;
      
          @ApiModelProperty("備忘(用戶無備忘的時候爲空)")
          private String memo;
      
          @ApiModelProperty("產品信息列表")
          private List<ProdInfo> prodInfoList;
      
          @Data
          public static class ProdInfo {
              @ApiModelProperty("商品ID")
              private String prodId;
      
              @ApiModelProperty("商品單價")
              private Long unitPriceInCent;
          }
      }
    

2.3 補充其他

2.3.1 DTO使用規範
  • 不是每個方法都需要將入參封裝成DTO類
  • 傳JSON必須使用DTO類
  • 傳鍵值對,參數比較少的,不要用DTO類,直接寫在控制器的方法裏列出
  • 傳鍵值對,如果字段比較多,允許使用DTO類,但不建議,直接平鋪到控制器方法更加直觀
  • 返回值要封裝統一的DTO,如ResponseDTO,一般有code、message、reqTime、data字段
  • 控制器的方法返回值,即丟在ResponseDTO裏的data,如果有返回值,建議data使用DTO裝起來,例如將UserDTO放在data裏
  • 如果控制器的方法返回值ResponseDTO裏的data比較簡單,也允許使用單一類型
  • DTO分爲reqDTO和respDTO,分別對應入參和出參,一般是一對的,不建議DTO繼承,禁止控制器不同方法複用同一個DTO禁止DTO裏存在沒用的字段
  • DTO有符合結構的,建議使用共有的靜態內部類,寫在同一個文件裏比較好找
  • DTO使用lombok,以方便閱讀
2.3.2 爲什麼會有上面的規範

上面的Swagger使用規範是根據實際情形制定出來的。首先使用繁瑣將不利於推廣,大浪淘沙後,就留下了上述少量的註解。

  • required=…在可以省略的情況下,即使頂着 “IDEA 提示 redundant” 還要堅持 “禁止省略”,爲什麼?

    爲了風格統一,爲了直觀。否則還需要在腦裏反應一會,才知道其默認值。尤其是 @RequestParam默認是true,但是@ApiParam的required默認false,容易搞混

  • 有些地方一定要用 @RequestParam,有些一定不要用,爲什麼?

    因爲 Swagger有bug,對於單一類型的字段,不寫會導致它認爲是傳JSON,但是對於DTO類型的入參,寫上之後展不開DTO裏面的字段值

  • 要求返回值DTO不要用 @ApiModelProperty的required屬性,爲什麼?

    因爲沒有意義,不增加開發者的負擔。又不是入參,必不必填不重要。如果說你用required表示字段會不會出現,這用法不好。寫在DTO裏的字段,即使它的值是null,在JSON格式化之後也是定會出現的;如果你說用required表示這個值會不會是null,建議真別這麼增加後端工作量,代碼一改就變還要維護這個屬性值

  • 要求入參DTO不要用example,即 @ApiModelProperty(example="…", required=…),爲什麼?

    • example的屬性,本意是參數示例值,這跟屬性的設計不一致

    • 在DTO,名叫A,裏面有個List的字段,B是一個類,如果在此字段使用 @ApiModelProperty(example="…", required=…),將不能展開B類裏的字段

    • 使用後,提交接口,對於Integer類型的,也會顯示成字串,提交失敗

    • 會拋出錯誤,例如遇到下面的用法時(拋出錯誤不影響使用,但是會在控制檯打印出來)

      // 這個明明是數字類型,example卻寫了字串,會導致AbstractSerializableParameter#getExample()
      // 拋出格式化錯誤的異常
      @ApiModelProperty(example = "售價", required = true)
      private Integer priceInCent;
      

Example

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

3. Swagger其他

3.1 如何更換UI

我們知道使用Swagger需要引入兩個GAV,其中UI的那個是可以替換的。新UI不僅僅是UI上的改變,還有導出md等重要功能。

官方的是io.springfox:springfox-swagger-ui,可以換成別的,注意不要同時存在多個UI,例如

<!-- bootstrap-ui,請求路徑:http://{host}:{port}/doc.html,覺得是最好的UI -->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>swagger-bootstrap-ui</artifactId>
    <version>1.9.6</version>
</dependency>

<!-- ui-layer,請求路徑:http://{host}:{port}/docs.html,覺得沒比原生的好,還不能用groupName,據說支持微服務 -->
<dependency>
    <groupId>com.github.caspar-chen</groupId>
    <artifactId>swagger-ui-layer</artifactId>
    <version>1.1.3</version>
</dependency>

推薦使用bootstrap-ui,這個UI美觀、緊湊、還支持全局按照請求路徑搜索,導出md等衆多特性

3.2 如何共存多套UI

其實是可以共存多個UI的,實際測試可行。注意有時候會發現其中一個UI能刷出來另外一個不行,可以稍微等一等再試,或者調換下一下兩個UI的順序再試下。總之我遇到過 “其中一個UI可以另外一個不行” 的情況,但是最終發現 “兩個UI可以並存”

3.3 如何支持微服務

微服務部署的時候確實比較麻煩,接口很分散,詳細可以搜下教程,本文不發散討論

3.4 NumberFormatException 問題

時不時會出現NumberFormatException異常,這是Swagger的bug,似乎滿足這些條件就會出現異常

  • 高版本,據說低版本的不會(詳細沒研究具體版本)
  • 必須是請求參數,響應參數不會
  • 鍵值對傳參方式,傳JSON時不會
  • @ApiModelProperty 中沒有設置example或者設置成空串
  • @ApiModelProperty 修飾的是數字類型(如Integer、Long等等)
// 具體的原因是,AbstractSerializableParameter#getExample
// 發生在 return Long.valueOf(example); 這行
// 可以發現,這個方法應該是Swagger頁面要展示Example Value,必須獲取example的值,當是數字類型的時候,空串被轉換時拋出異常
// 同時可以看到,即使發生異常,也無關緊要,第一這是展示示例
@JsonProperty("x-example")
public Object getExample() {
    if (example == null) {
        return null;
    }
    try {
        if (BaseIntegerProperty.TYPE.equals(type)) {
            return Long.valueOf(example);
        } else if (DecimalProperty.TYPE.equals(type)) {
            return Double.valueOf(example);
        } else if (BooleanProperty.TYPE.equals(type)) {
            if ("true".equalsIgnoreCase(example) || "false".equalsIgnoreCase(defaultValue)) {
                return Boolean.valueOf(example);
            }
        }
    } catch (NumberFormatException e) {
        LOGGER.warn(String.format("Illegal DefaultValue %s for parameter type %s", defaultValue, type), e);
    }
    return example;
}

解決方法
換JAR包,詳細見下面的搭建過程已經排除了這個問題了

需要注意的是,2.8.0版本的swagger,不會出現這個問題(經過實際測試了),2.8.0的代碼也是`if (example == null)` 這麼判斷,爲什麼就不會出現這個問題呢? 暫時不知道爲什麼


@JsonProperty("x-example")
public Object getExample() {
    if (example == null) {
        return null;
    }
    ...
}

3.5 必填參數顯示不準確的問題

沒辦法解決

說的是這個問題

@ApiOperation("新增訂單")
@PostMapping("/order/add")
public ResponseDTO<OrderRespDTO> addOrder(
        @ApiParam(value = "訂單信息", required = true) @Valid @RequestBody OrderReqDTO orderReqDTO
)


@Data
public class OrderReqDTO {
    @NotEmpty
    @Valid
    @ApiModelProperty(value = "商品信息列表", required = true)
    private List<ProdInfo> prodInfoList;

    @NotNull
    @ApiModelProperty(value = "商品總價", required = true)
    private Long totalPriceInCent;

    @ApiModelProperty(value = "備註", required = false)
    private String memo;



    @Data
    public static class ProdInfo {
        @ApiModelProperty(value = "商品ID", required = true)
        @NotBlank
        private String prodId;

        @ApiModelProperty(value = "商品單價", required = true)
        @NotNull
        @Min(0)
        private Long unitPriceInCent;
    }
}

實際上可以看到 prodInfoList 裏的 ProdInfo 的兩個字段都被標註爲必填,但是界面上卻顯示非必填

在這裏插入圖片描述
最初我以爲是bootstrap-ui的問題,後來看官方UI也是這樣子。
在這裏插入圖片描述

4. Springboot+Swagger2搭建步驟

演示了springboot結合swagger2的

  1. pom.xml 中添加(儘量用新版本)
<!-- Swagger2 BEGIN -->
<!--
swagger2包中swagger-models版本有bug,example爲空串會爆出NumberFormatException,需要排除並引入高版本
排除時將 swagger-models 和 swagger-annotations 整一對排除
-->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
    <exclusions>
        <exclusion>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-models</artifactId>
        </exclusion>
        <exclusion>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-models</artifactId>
    <version>1.6.0</version>
</dependency>
<dependency>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-annotations</artifactId>
    <version>1.6.0</version>
</dependency>

<!-- 不滿意可以註釋掉換其他UI,可以同時開啓多個UI -->
<!-- 官方UI,請求路徑 http://{host}:{port}/swagger-ui.html -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>


<!-- bootstrap-ui,請求路徑:http://{host}:{port}/doc.html,覺得是最好的UI -->
<!-- <dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>swagger-bootstrap-ui</artifactId>
    <version>1.9.6</version>
</dependency> -->


<!-- ui-layer,請求路徑:http://{host}:{port}/docs.html,覺得沒比原生的好 -->
<!--<dependency>
    <groupId>com.github.caspar-chen</groupId>
    <artifactId>swagger-ui-layer</artifactId>
    <version>1.1.3</version>
</dependency>-->
<!-- Swagger2 END -->
  1. 寫一個 Swagger2Config 配置類(詳細見後面的附錄)
1)標註:@Configuration 和 @EnableSwagger2
2)產生一個 Docket 的Bean
  1. 啓動springboot並訪問:http://{host}:{port}/swagger-ui.html(使用的ui不同則不同)
  2. 附錄:Swagger2Config 類代碼,需要修改basePackage,掃描註解產生文檔
@Configuration
@EnableSwagger2
public class Swagger2Config {
    @Bean
    public Docket docket() {
        // basePackage 需要掃描註解生成文檔的路徑
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.wyf.test.swagger2springboot"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Swagger demo")
                .description("這是展示Swagger怎麼用的例子")
                .version("1.0").build();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章