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的
- 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 -->
- 寫一個 Swagger2Config 配置類(詳細見後面的附錄)
1)標註:@Configuration 和 @EnableSwagger2
2)產生一個 Docket 的Bean
- 啓動springboot並訪問:http://{host}:{port}/swagger-ui.html(使用的ui不同則不同)
- 附錄: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();
}
}