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();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章