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就够了
在如下地方使用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();
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/135277.html