SpringBoot整合Swagger2流程,超详细!

本文最后更新于:4 个月前

前言

本文将介绍下API可视化框架Swagger在SpringBoot框架中的整合流程,对在整合过程中遇到的问题进行讨论,并对Swagger2进行测试的一系列内容。

Swagger简述

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法,参数和模型紧密集成到服务器端的代码,允许API来始终保持同步。Swagger 让部署管理和使用功能强大的API从未如此简单。

前期准备

在开始整合之前,需要先创建SpringBoot项目,本文所用到的工具及版本如下:

项目内容
IDEIntelliJ IDEA
Java万年不变Java8
SpringBoot2.2.5.RELEASE

SpringBoot版本高于2.6时默认路径匹配方式为PathPatternMatcher,而Swagger2基于的是AntPathMatcher,会出现documentationPluginsBootstrapper’; nested exception is java.lang.NullPointer错误,这里需要千万注意两者的兼容性!!!

引入依赖

本文将使用Maven作为管理工具进行探讨。引入Swagger2有两种方式,分别为starter方式和原生Maven依赖方式。

原生Maven依赖

Swagger2的依赖可到Maven仓库中找到,所用到的版本为2.9.2,这里贴出依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- swagger start -->
<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>
<!-- swagger end -->

Starter引入

SpringForAll下的spring-boot-starter-swagger利用Spring Boot的自动化配置特性来实现快速的将swagger2引入spring boot应用来生成API文档,简化原生使用swagger2的整合代码。

在spring-boot-starter-swagger的GitHub页面有着超级详细的介绍以及整合教程,此处不再进行过多的赘述,有需要此方式整合的小伙伴请移步spring-boot-starter-swagger

其他依赖

Lombok

除了swagger2的依赖和SpringBoot的依赖之外,我们再引入Lombok,来减少一些 get/set/toString 方法的编写,合理的使用前人做好的轮子:

1
2
3
4
5
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>

当然,在使用IDEA创建项目的时候顺手引入Lombok也不是不行。

commons-lang3

commons-lang3中有大量的工具类,这里我们也引用进来:

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>

配置

此处依然是重点介绍原生Maven依赖方式整合,与Starter采取配置文件方式进行配置不同的是,原生方式需要使用配置类进行配置;

当然,在本文中我们也会将配置类与配置文件进行配合使用,同样可解决配置不够灵活的问题。

SwaggerProperties

这里我们将Swagger2配置类所需的参数封装到Properties类中,在src/main/java的包中,创建config/properties包用于存放Properties类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.javafeng.boxcloud.config.properties;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.stereotype.Component;

import java.util.List;

@Data
@Component
@ConfigurationProperties(prefix = "swagger")
public class SwaggerProperties {
// 是否启用Swagger
private boolean enable;

// 扫描的基本包
@Value("${swagger.base.package}")
private String basePackage;

// 联系人邮箱
@Value("${swagger.contact.email}")
private String contactEmail;

// 联系人名称
@Value("${swagger.contact.name}")
private String contactName;

// 联系人网址
@Value("${swagger.contact.url}")
private String contactUrl;

// 描述
private String description;

// 标题
private String title;

// 网址
private String url;

// 版本
private String version;
}

application.yml

接下来配置application.yml或者application.properties配置文件,本文使用的是application.yml,此处是与Properties类一致的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
application:
name: BoxCloud
swagger:
# 是否启用
enable: true
base:
# 扫描的包,多个包使用逗号隔开
package: com.javafeng
contact:
email: blog@javafeng.com
name: JAVAFENG
url: https://www.javafeng.com
description:
title: ${spring.spring.name} API Document
url: https://www.javafeng.com
version: @project.version@

Swagger2Config

参数配置完后,进行Swagger2Config的配置,此处参考了spring-boot-plus的配置方式,进行了一些简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package com.javafeng.boxcloud.config;

import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.javafeng.boxcloud.config.properties.SwaggerProperties;
import io.swagger.annotations.Api;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.RequestHandler;
import springfox.documentation.annotations.ApiIgnore;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.ApiSelectorBuilder;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Arrays;

@Configuration
@EnableSwagger2
public class Swagger2Config {
@Autowired
private SwaggerProperties swaggerProperties;

// 扫描多包时,包路径的拆分符,分号
private static final String SPLIT_COMMA = ",";

// 扫描多包时,包路径的拆分符,逗号
private static final String SPLIT_SEMICOLON = ";";

// Swagger忽略的参数类型
private Class<?>[] ignoredParameterTypes = new Class[]{
ServletRequest.class,
ServletResponse.class,
HttpServletRequest.class,
HttpServletResponse.class,
HttpSession.class,
ApiIgnore.class
};

@Bean
public Docket createRestApi() {
// 获取需要扫描的包
String[] basePackages = getBasePackages();
ApiSelectorBuilder apiSelectorBuilder = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select();
// 如果扫描的包为空,则默认扫描类上有@Api注解的类
if (ArrayUtils.isEmpty(basePackages)) {
apiSelectorBuilder.apis(RequestHandlerSelectors.withClassAnnotation(Api.class));
} else {
// 扫描指定的包
apiSelectorBuilder.apis(basePackage(basePackages));
}
Docket docket = apiSelectorBuilder.paths(PathSelectors.any())
.build()
.enable(swaggerProperties.isEnable())
.ignoredParameterTypes(ignoredParameterTypes);
return docket;
}

/**
* 获取apiInfo
* @return
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(swaggerProperties.getTitle())
.description(swaggerProperties.getDescription())
.termsOfServiceUrl(swaggerProperties.getUrl())
.contact(new Contact(swaggerProperties.getContactName(), swaggerProperties.getContactUrl(), swaggerProperties.getContactEmail()))
.version(swaggerProperties.getVersion())
.build();
}

/**
* 获取扫描的包
*
* @return
*/
public String[] getBasePackages() {
String basePackage = swaggerProperties.getBasePackage();
if (StringUtils.isBlank(basePackage)) {
throw new RuntimeException("Swagger basePackage不能为空");
}
String[] basePackages = null;
if (basePackage.contains(SPLIT_COMMA)) {
basePackages = basePackage.split(SPLIT_COMMA);
} else if (basePackage.contains(SPLIT_SEMICOLON)) {
basePackages = basePackage.split(SPLIT_SEMICOLON);
}
return basePackages;
}

public static Predicate<RequestHandler> basePackage(final String[] basePackages) {
return input -> declaringClass(input).transform(handlerPackage(basePackages)).or(true);
}

private static Function<Class<?>, Boolean> handlerPackage(final String[] basePackages) {
return input -> {
// 循环判断匹配
for (String strPackage : basePackages) {
boolean isMatch = input.getPackage().getName().startsWith(strPackage);
if (isMatch) {
return true;
}
}
return false;
};
}
@SuppressWarnings("deprecation")
private static Optional<? extends Class<?>> declaringClass(RequestHandler input) {
return Optional.fromNullable(input.declaringClass());
}
}

knife4j增强

因为增强的配置较为简单,且使用增强效率更高,所以将此部分内容提前。不和前面的一起讲解的原因是为了更好地区分哪些是Swagger的哪些是knife4j的,更容易理解。

本文中我们整合knife4j作为Swagger的增强,在pom中添加依赖:

1
2
3
4
5
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>

在application.yml中添加如下配置:

1
2
3
4
5
6
knife4j:
enable: ${swagger.enable} -- knife4j启用与否取决于Swagger是否启用
basic:
enable: true
username: admin
password: admin

修改Swagger2Config,添加启用knife4j的相关注解:

1
2
3
4
5
6
7
......
@Configuration
@EnableSwagger2
@EnableKnife4j
public class Swagger2Config {
......
}

对于knife4j更加详细的配置,自行按需到官网查询即可。

knife4j 2.0.6以上版本,无需使用@EnableKnife4j注解,直接在配置文件中配置knife4j.enable = true 即可。此处为了进行区分,使用2.0.2进行演示。

测试

上述步骤完成后,基本上前期配置工作就结束了,可以直接浏览器访问:http://localhost:8080/swagger-ui.html ,出现以下界面表示配置成功:

Swagger

因为配置了增强,则推荐使用knife4j进行查看,访问http://localhost:8080/doc.html ,出现如下界面则表示增强配置成功(在此页面前会有要求输入用户名密码的页面,按照配置中的填写即可):

knife4j

使用

此部分截图均为knife4j页面截图,显示效果和页面逻辑更加清晰

接下来我们创建Controller并声明一些接口进行测试,这里我们来通过对用户增删查改、头像上传、登录等功能的模拟,来全方位展示如何使用Swagger2。在类中出现的Swagger2注解,会在类后有相应的介绍(只对常用属性进行了介绍,若需要深层次研究,建议到官网查阅文档)。

这里实际上有几种情况需要加以区分:

  • 入参是否为实体类
  • 响应是否为实体类

简单分析,上述的几个Api接口中:

  • 新增接口、修改接口、登录接口等入参为实体类,其余入参为非实体类
  • 查找接口响应为实体类,其余响应为非实体类

根据不同的情况,使用不同的Swagger注解进行处理。

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import lombok.Data;

@Data
@ApiModel(value = "用户", description = "查询用户")
public class Users {
@ApiModelProperty(value = "ID", example = "1")
private Integer id;
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "头像")
private String avatar;

public Users(Integer id, String username, String password, String avatar) {
this.id = id;
this.username = username;
this.password = password;
this.avatar = avatar;
}

public Users() {
}
}

@ApiModel

注解/常用属性说明
@ApiModel用于修饰实体类(模型),可视作对实体类的说明
 value模型的备用名称
 description该类的详细说明

@ApiModelProperty

注解/常用属性说明
@ApiModelProperty用于修饰实体类字段,可视作对字段的各方面描述与限制
 value字段的说明
 name覆盖原字段名的新字段名
 example默认值(当此字段为String类型时默认值为 “”)
 allowableValues限制该字段值取值范围,表示为限定值列表({1,2,3})、范围值([1,5])、
最大/最小值([1, infinity] infinity或-infinity表示无限值)
 required标记该字段是否必需,默认false
 hidden标记该字段是否隐藏,默认false

当使用实体类作为入参时,可使用上述注解对实体类进行修饰,以达到描述实体类参数的目的。

控制器

控制器这里,分情况进行分别讲解,首先是Controller控制器类的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.javafeng.boxcloud.controller;

import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
@Api(tags = {"用户管理"})
public class UsersController {
/** 此处编写Api接口方法 **/
/** --start-- **/
/** --end-- **/
}
注解/常用属性说明
@Api用于修饰Controller,可将此Controller视作一个接口组,该类中的Api将自动生成文档
 tags可视为该接口组的名称

接下来是接口方法的定义,在本节开始处,我们就了解了接口方法的集中参数情况和响应情况,这里针对这几种不同的情况分别进行讲解。

  1. 实体类作为入参

新增接口、修改接口均为实体类作为入参的情况:

1
2
3
4
5
6
7
8
9
10
11
@PostMapping("/add")
@ApiOperation(value = "新增用户", notes = "用户不得为空")
public Boolean add(@RequestBody Users user) {
return true;
}

@PostMapping("/update")
@ApiOperation(value = "修改用户", notes = "用户不得为空")
public Boolean update(@RequestBody Users user) {
return true;
}

@ApiOperation

注解/常用属性说明
@ApiOperation用于修饰Controller,可将此Controller视作一个接口组,该类中的Api将自动生成文档
 valueApi接口名称
 notes接口描述
 tags定义额外的接口组
例如:即使在@Api(tags = ‘用户管理’)定义了接口组,也可以在此注解内指定另一个接口组,如@ApiOperation(tags = ‘账号管理’),使此接口可以同时出现在这两个接口组(用户管理 和 账号管理)中

当实体类作为入参时,我们在上一部分【实体类】中已经介绍了相应的参数描述注解以及用法,除@ApiOperation外,此处不再对其他注解(@ApiModel、@ApiModelProperty)进行赘述。

实体类入参

  1. 非实体类作为入参

当非实体类作为入参时,又可以细分为以下几种情况:

  • 普通查询参数

这里我们将查找某个用户的参数定义为普通参数的Api方法:

1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("/get")
@ApiOperation(value = "查询单个用户")
@ApiImplicitParams({
@ApiImplicitParam(name = "id",
value = "用户ID",
required = true,
paramType = "query"
)
})
public Users get(@RequestParam("id") Integer id) {
return new Users(id, "admin", "123456", "/resource/image/head.png");
}

@ApiImplicitParams 和 ApiImplicitParam

注解/常用属性说明
@ApiImplicitParams修饰Api接口方法,用来声明请求参数
 @ApiImplicitParam定义在 @ApiImplicitParams,每个@ApiImplicitParam 对应一个参数
  name参数名称
[通常与入参名对应]
  value参数说明
  required标记该参数是否必需,默认false
  paramType标记该参数的位置,包含path、query、body、form、header等几种情况
[一般情况下,body、form推荐使用实体类作为入参]

普通Query参数

  • 路径参数
1
2
3
4
5
6
7
8
9
10
11
12
@DeleteMapping("/delete/{id}")
@ApiOperation(value = "删除用户", notes = "用户ID不得为空")
@ApiImplicitParams({
@ApiImplicitParam(name = "id",
value = "用户ID",
required = true,
paramType = "path"
)
})
public Boolean delete(@PathVariable("id") Integer id) {
return true;
}

当参数为路径参数时,@ApiImplicitParam的paramType取值应为path,同时,使用@PathVariable注解修饰该路径参数。

@PathVariable注解能够识别URL里面的一个模板,如上述接口的{id},并且标记该参数是基于该模板获取的,并不属于Swagger注解,此处不做过多介绍。

路径参数

  • header参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@PostMapping("/login")
@ApiOperation(value = "登录")
@ApiImplicitParams({
@ApiImplicitParam(name = "username",
value = "用户名",
required = true,
paramType = "header"
),
@ApiImplicitParam(name = "password",
value = "密码",
required = true,
paramType = "header"
)
})
public Boolean login(@RequestHeader("username") String username,@RequestHeader("password") String password) {
System.out.println(username + password);
return true;
}

当参数为路径参数时,@ApiImplicitParam的paramType取值应为header,同时,使用@RequestHeader注解对参数进行修饰,将参数的获取位置标记为从Header中获取,让SpringMVC能够正确的从Header中获取到参数。

Header参数

  • 文件参数

这里我们直接讲解一种复杂情况,即考虑同时包含文件参数和普通参数,在实际开发过程中按需修改即可。

1
2
3
4
5
6
7
8
9
10
11
12
@PostMapping("/upload")
@ApiOperation(value = "上传头像", notes = "参数需要头像文件以及对应用户ID")
@ApiImplicitParams({
@ApiImplicitParam(name = "id",
value = "该头像对应的用户ID",
required = true
)
})
public Boolean upload(@ApiParam(value = "图片文件", required = true) @RequestParam("avatar") MultipartFile avatar, @RequestParam("id") Integer id) {
System.out.println(avatar);
return true;
}

对于非文件参数的普通参数,参照第一条【普通查询参数】中的声明方式即可;

对于文件参数,则需要使用@ApiParam对参数进行修饰;

@ApiParam

注解/常用属性说明
@ApiImplicitParams修饰Api接口方法,用来声明请求参数
 value参数名称
 required标记该参数是否必需,默认false
 allowMultiple是否允许多个文件,默认false

同样的,需要在参数上使用@RequestParam进行修饰。

文件参数

这里插入一个解释,就是对于同时出现Swagger注解(一般以Api作为前缀)和非Swagger注解同时对参数进行修饰时,并不会彼此影响,Swagger注解只是用来对Api方法进行描述,并不会对该方法造成实质影响,

而例如@RequestParam、@RequestHeader在内的注解是决定SpringMVC是否能正确读取到参数的关键

这里对于两种注解之间的侵入性,掘金大佬1黄鹰在其源码剖析@ApiImplicitParam对@RequestParam的required属性的侵入性做了深入剖析,感兴趣的可以去看看

  1. 实体类作为响应

当实体类作为响应时,通常在实体类上的注解所生成的描述也会作为响应的描述出现,在此处不再进行赘述。

实体类响应

  1. 非实体类作为响应

我们对新增接口进行修改,在其方法上添加@ApiResponses来对响应进行描述:

1
2
3
4
5
6
7
8
9
10
@PostMapping("/add")
@ApiOperation(value = "新增用户", notes = "用户不得为空")
@ApiResponses({
@ApiResponse(code = 200, message = "添加成功"),
@ApiResponse(code = 500, message = "服务器错误"),
@ApiResponse(code = 400, message = "参数异常")
})
public Boolean add(@RequestBody Users user) {
return true;
}

需要注意的是,Swagger无法对非实体类响应进行详细描述,只能通过@ApiResponses和@ApiResponse描述响应码信息。同时,在以实体类作为响应时,同样可也以使用@ApiResponses和@ApiResponse。

非实体类响应

Token处理

在做前后端分离的应用时,后端接口通常会要求在Header中添加Token以保证安全性,这里我们依然是参考SpringBootPlus的处理方式,对Token进行处理。

需要注意的是,此处的Token处理是基于Swagger进行接口测试时的,并不是对接口如何增加Token进行讲解,只是对有Token的接口如何通过Swagger进行测试做出讲解。

修改SwaggerProperties

思路是,在Swagger配置中添加默认的全局参数描述,对Token进行处理,这里我们默认Token信息附加在Header中。

首先在SwaggerProperties中新增以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@NestedConfigurationProperty
private List<ParameterConfig> parameterConfig;

// 自定义参数配置
@Data
public static class ParameterConfig {
// 名称
private String name;
// 描述
private String description;
// 参数类型
// header, cookie, body, query
private String type = "head";
// 数据类型
private String dataType = "String";
// 是否必填
private boolean required;
// 默认值
private String defaultValue;
}

修改application.yml

随后在application.yml中对新增部分编写对应的配置项,这里贴出整体内容,自定义参数配置部分为新增内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
spring:
application:
name: BoxCloud

swagger:
enable: true
base:
package: com.javafeng
contact:
email: blog@javafeng.com
name: JAVAFENG
url: https://www.javafeng.com
description:
title: ${spring.application.name} API Document
url: https://www.javafeng.com
version: @project.version@
# 自定义参数配置,可配置N个
parameter-config:
- name: token
description: Token Request Header
# header, cookie, body, query
type: header
data-type: String
required: false
# 测试接口时,自动填充token的值
default-value:

knife4j:
enable: ${swagger.enable}
basic:
enable: true
username: admin
password: admin

这里可以根据不同的需求,配置多个自定义参数,这里只演示了Token一个参数,如果是多个参数的话,配置多个即可,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
parameter-config:
- name: param1
description: This is param1
type: header
data-type: String
required: false
default-value:
parameter-config:
- name: param2
description: This is param2
type: header
data-type: String
required: false
default-value:
parameter-config:
- name: param3
description: This is param3
type: header
data-type: String
required: false
default-value:

修改Swagger2Config

接下来,我们在Swagger2Config中对Token参数进行处理,首先在Swagger2Config中添加如下方法,从application.yml中获取到配置的额外Token参数并进行封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 添加额外参数
*
* @return
*/
private List<Parameter> getParameters() {
// 获取自定义参数配置
List<SwaggerProperties.ParameterConfig> parameterConfig = swaggerProperties.getParameterConfig();
if (CollectionUtils.isEmpty(parameterConfig)) {
return null;
}
List<Parameter> parameters = new ArrayList<>();
parameterConfig.forEach(parameter -> {
// 设置自定义参数
parameters.add(new ParameterBuilder()
.name(parameter.getName())
.description(parameter.getDescription())
.modelRef(new ModelRef(parameter.getDataType()))
.parameterType(parameter.getType())
.required(parameter.isRequired())
.defaultValue(parameter.getDefaultValue())
.build());
});
return parameters;
}

随后修改createRestApi方法,在声明Docket的位置添加对额外参数的处理,添加后如下:

1
2
3
4
5
6
7
... ...
Docket docket = apiSelectorBuilder.paths(PathSelectors.any())
.build()
.enable(swaggerProperties.isEnable())
.ignoredParameterTypes(ignoredParameterTypes)
.globalOperationParameters(getParameters()); // 此处为新增
... ...

重启项目后,我们随意打开一个接口的文档,这里我们打开的是knife4j的页面,选择调试,在请求头部位置就可以看到Token的相关内容:

knife4j

当然,在Swagger的原生界面也可以看到:

swagger

参考和引用

  1. dynamicbeamswagger2常用注解API,来源 CSDN
  2. 随风行云Spring Boot整合swagger使用教程,来源 CNBLOG
  3. 1黄鹰源码剖析@ApiImplicitParam对@RequestParam的required属性的侵入性, 来源 掘金
  4. SpringBootPlus

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!