通常开发初期架构师需要给出DB设计、API接口设计,
而API接口设计或者以文档(word文档、markdown等)、或者以在线API文档(swagger-ui、yapi、showdoc等)形式给出,
以设计先行模式(本人也比较推崇此模式)
为例,架构师通常通过如下两种方式输出API文档:
记得N年之前用的是公司统一规范的word文档进行API文档编写,
后续迁移到了在线文档平台YAPI(兼容Swagger 2.0),
迁移过程中写了工具把word文档转换成swagger.json,然后再导入YAPI中(在此基础上再手动修改),
后续用的比较多的就是直接在YAPI编写文档。
之前一直不用Swagger是觉得这个东西太重了,需要在代码中添加好多和业务无关的注解,
但是Swagger这个文档规范还是很通用的(业界标准),
在编写Swagger注解的同时也是在定义程序接口框架(输出代码,提高后续开发效率),
所以就萌生了不通过Swagger注解 而是结合Java代码注释
就可以生成Swagger统一规范文档(后续可导入其他在线API平台)
的想法,
注: 代码及注释可以借助DB定义通过代码生成工具进行生成,而后再进一步修改
本文就是在实现此种想法过程中的一些记录,
这个过程的探索也借助了业内比较通用的工具:
接下来依次对各工具进行介绍与集成示例讲解。
Swagger提供了一整套API设计、文档编写及展示的工具,提供开源版、企业版、cloud版,
开源版主要包括:
RESTful API
的接口定义规范,目前支持OAS 2.0和OAS 3.0(前身也叫Swagger规范,在2015后捐赠给Linux Foundation后更名为OpenAPI)OAS规范的发展历史见下表(目前最新版本为OAS 3.0.3):
版本 | 发布日期 | 说明 |
---|---|---|
3.0.3 | 2020-02-20 | Patch release of the OpenAPI Specification 3.0.3 |
3.0.2 | 2018-10-08 | Patch release of the OpenAPI Specification 3.0.2 |
3.0.1 | 2017-12-06 | Patch release of the OpenAPI Specification 3.0.1 |
3.0.0 | 2017-07-26 | Release of the OpenAPI Specification 3.0.0 |
3.0.0-rc2 | 2017-06-16 | rc2 of the 3.0 specification |
3.0.0-rc1 | 2017-04-27 | rc1 of the 3.0 specification |
3.0.0-rc0 | 2017-02-28 | Implementer’s Draft of the 3.0 specification |
2.0 | 2015-12-31 | Donation of Swagger 2.0 to the OpenAPI Initiative |
2.0 | 2014-09-08 | Release of Swagger 2.0 |
1.2 | 2014-03-14 | Initial release of the formal document. |
1.1 | 2012-08-22 | Release of Swagger 1.1 |
1.0 | 2011-08-10 | First release of the Swagger Specification |
Swagger 2.0和OAS 3.0常用注解对比:
Swagger 2.0 包名:io.swagger.annotations |
OAS 3.0 包名:io.swagger.v3.oas.annotations |
注解常用位置 |
---|---|---|
@Api | @Tag(name = “接口类描述”) | Constroller类上 |
@ApiOperation(value = “foo”, notes = “bar”) | @Operation(summary = “foo”, description = “bar”) | Controller方法上 |
@ApiImplicitParams | @Parameters | Controller方法上 |
@ApiImplicitParam | @Parameter(name = “id”, description = “用户ID”, example = “1”, in = ParameterIn.QUERY) | Controller方法上、参数前、@Parameters里 |
@ApiParam | @Parameter | Controller方法参数前 或者 @Operation.parameters里 |
@ApiResponse(code = 404, message = “foo”) | @ApiResponse(responseCode = “404”, description = “foo”) | Controller类上、方法上、@Operation.responses里 |
@ApiIgnore | @Parameter(hidden = true) @Operation(hidden = true) @Hidden |
Controller类上、方法上、Model对象上、属性上 |
@ApiModel | @Schema(description = “对象描述”) | Model对象类上 |
@ApiModelProperty(hidden = true) | @Schema(accessMode = READ_ONLY) | Model对象属性上 |
@ApiModelProperty | @Schema(description = “用户性别(1:男,2:女)”, required = true, example = “1”) | Mode对象属性上 |
Springfox 和 Springdoc均是spring社区(非官方)开发的,
支持在Spring生态中根据 SpringMvc代码、Swagger注解、JSR303注解(@NotNull, @Min, @Max, @Size) 自动生成接口文档的工具,并且支持集成Swagger UI。
而Smart-doc则是国内开源的根据SpringMvc代码、Java源码注释、JSR303注解、泛型推导等自动生成接口文档的工具。
Springfox支持Swagger 2.0 和 OAS 3.0规范,
对Swagger 2.0的支持较为成熟,比较流行,
但对OAS 3.0的支持并不完善(可参见springfox 3.0整合OAS 3.0及其问题),
目前社区也不是很活跃(目前最新版本3.0.0发布在2020-07-14,已经1年多没有更新了)。
SpringFox 3.0.0 的新特性:
而Springdoc仅支持OAS 3.0规范,但对OAS 3.0的支持比较完善,
社区较为活跃(目前最新版本1.6.4发布在2022-01-06),
Springdoc核心特性如下:
从 SpringFox 迁移到 SpringDoc
可参见官网文档:Migrating from SpringFox
Smart-doc为国内开源的根据Java源码注释
、JSR303注解
、泛型推导
等自动生成接口文档的工具。你只需要按照java-doc标准编写注释,smart-doc就能帮你生成一个简易明了的Markdown、HTML5、Postman Collection2.0+、OpenAPI 3.0+的文档。
smart-doc最吸引我的特性就是基于注释
,而不是基于注解
,
我们在写Java代码的时候,都要求书写规范的文档注释,
而使用Swagger生态则需要侵入代码再书写大量swagger-core注解,
如@Api, @ApiOperation,…,@Tag,@Operation,…,增加了一定学习成本,
作为开发人员会觉得很痛苦,同样的说明功能被做了2次(注释、注解),后续维护也要修改2次,
而且代码中需要添大量和业务无关的Swagger-core注解,
而使用smart-doc可以直接提取我们文档中的注释来生成API文档,不需要再添加额外的注解。
同时smart-doc支持生成多种文档格式:
smart-doc的相关特性如下:
零注解
、零学习成本、只需要写标准JAVA注释
。源代码
接口定义自动推导,强大的返回结构推导。Spring MVC、Spring Boot、Spring Boot Web Flux(controller书写方式)、Feign
。JSR303
参数校验规范,包括分组
验证。错误码
和定义在代码中的各种字典码
到接口文档。Maven、Gradle插件
式轻松集成。Apache Dubbo RPC
接口文档生成。Markdown、HTML5、Asciidoctor、Postman Collection、OpenAPI 3.0
。debug
接口调试html5页面完全支持文件上传,下载(@download tag标记下载方法)测试。接入文档管理系统
。smart-doc关于注释也支持一些特殊格式:
实际使用smart-doc中遇到的问题:
框架 | Swagger 2.0 | OAS 3.0 | 依赖Swagger注解 (侵入代码) |
依赖Java注释 (不侵入代码) |
社区活跃度 | 官网及源码仓库 |
---|---|---|---|---|---|---|
springfox | ✔️springfox2 成熟 |
✔️springfox3 OAS 3.0支持不完善,兼容Swagger 2.0 |
✔️ | ❌ | 不活跃 最新版本3.0.0 发布时间2020-07-14 |
http://springfox.github.io/springfox/ https://github.com/springfox/springfox |
springdoc | ❌ | ✔️springdoc 仅支持OAS 3.0 |
✔️ | ❌ | 活跃 最新版本1.6.4 发布时间2022-01-06 |
https://springdoc.org/ https://github.com/springdoc/springdoc-openapi |
smart-doc | ❌ | ✔️smart-doc 支持根据注释生成OAS 3.0文档 |
❌ | ✔️ | 活跃 最新版本2.3.6 发布时间2022-01-02 |
https://smart-doc-group.github.io/#/zh-cn/?id=smart-doc https://github.com/smart-doc-group/smart-doc https://gitee.com/smart-doc-team/smart-doc |
综上,
maven依赖:
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.2version>
<relativePath/>
parent>
<properties>
<springfox2.version>2.9.2springfox2.version>
<swagger.version>1.6.4swagger.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>${springfox2.version}version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>${springfox2.version}version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-bean-validatorsartifactId>
<version>${springfox2.version}version>
dependency>
<dependency>
<groupId>io.swaggergroupId>
<artifactId>swagger-annotationsartifactId>
<version>${swagger.version}version>
dependency>
<dependency>
<groupId>io.swaggergroupId>
<artifactId>swagger-modelsartifactId>
<version>${swagger.version}version>
dependency>
dependencies>
应用配置application.yaml:
spring:
mvc:
pathmatch:
# 设置path匹配策略,解决高版本springboot启动springfox报空指针异常问题,
# 具体参见:https://blog.csdn.net/Faint35799/article/details/122344731
matching-strategy: ant_path_matcher
代码配置:
import com.luo.demo.sc.base.enums.RespCodeEnum;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RequestMethod;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.ResponseMessage;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Springfox配置
*
* @author luohq
* @date 2022-01-15 17:33
*/
@Configuration
@EnableSwagger2
public class SpringfoxConfig {
@Bean
public Docket createRestApi() {
List<ResponseMessage> respMsgList = this.convertRespMsgList();
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
//设置全局响应信息
.globalResponseMessage(RequestMethod.GET, respMsgList)
.globalResponseMessage(RequestMethod.POST, respMsgList)
.globalResponseMessage(RequestMethod.PUT, respMsgList)
.globalResponseMessage(RequestMethod.DELETE, respMsgList)
.select()
//为当前包路径
.apis(RequestHandlerSelectors.basePackage("com.luo.demo.api.controller"))
.paths(PathSelectors.any())
.build();
}
@Bean
public ApiInfo apiInfo() {
return new ApiInfoBuilder()
// 页面标题
.title("Springfox Swagger2 - RESTful API")
// 创建人信息
.contact(new Contact("luohq", "https://blog.csdn.net/luo15242208310", "[email protected]"))
// 版本号
.version("1.0")
// 描述
.description("Springfox Swagger2构建RESTful API")
.build();
}
/**
* 转换响应码枚举RespCodeEnum为响应信息列表
*
* @return 响应信息列表
*/
private List<ResponseMessage> convertRespMsgList() {
return Stream.of(RespCodeEnum.values())
.map(respCodeEnum -> {
return new ResponseMessageBuilder()
.code(respCodeEnum.getCode())
.message(respCodeEnum.getMessage())
.build();
}).collect(Collectors.toList());
}
}
如上配置完成后,则可以直接访问swagger-ui界面:http://localhost:8080/swagger-ui.html
在不添加任何Swagger 2.0注解的情况下,Springfox也可根据SpringMvc相关结构生成文档如下图:
可以发现在英文环境下,规范的Controller方法名、参数名称、变量名称皆可起到说明的作用,
但是在中文环境这种纯英文的描述还不够,我们还需要中文的描述,
如此便可通过Swagger 2.0注解(或者后续的OAS 3.0注解、中文注释)的进行详细的中文说明。
添加Swagger 2.0注解的示例代码如下:
//=======================================================================================
//================================ Controller层代码 ======================================
//=======================================================================================
@Api(description = "用户信息管理")
@Slf4j
@RestController
@RequestMapping("/users")
@Validated
public class UserController {
@ApiOperation(value = "查询用户信息", notes = "根据用户ID查询用户信息")
@ApiImplicitParam(name = "id", value = "用户ID", paramType = "path", required = true)
@GetMapping("/{id}")
public RespResult<UserInfo> getUser(@NotNull @PathVariable Long id) {
log.info("get user, param: id={}", id);
return RespResult.successData(this.buildUser(id));
}
@ApiOperation(value = "查询用户信息列表", notes = "查询用户信息列表")
@GetMapping
public RespResult<UserInfo> getUsers(@Validated UserQueryDto userQueryDto) {
log.info("get users, param: {}", userQueryDto);
return RespResult.successRows(TOTAL_DEFAULT, this.buildUsers(Optional.ofNullable(userQueryDto.getId()).orElse(ID_DEFAULT), TOTAL_DEFAULT));
}
@ApiOperation(value = "新增用户及设备绑定信息")
@PostMapping
public RespResult<Integer> addUser(@Validated @RequestBody UserAddDto userAddDto) {
log.info("add user, param: {}", userAddDto);
return RespResult.successData(1);
}
@ApiOperation(value = "修改用户及设备绑定信息")
@PutMapping
public RespResult<Integer> updateUser(@Validated @RequestBody UserEditDto userEditDto) {
log.info("update user, param: {}", userEditDto);
return RespResult.successData(1);
}
@ApiOperation(value = "删除用户信息", notes = "根据用户ID列表删除用户信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "ids", value = "用户ID列表(逗号分隔)", paramType = "path", required = true)
})
@DeleteMapping("/{ids}")
public RespResult<Integer> deleteUsers(@NotEmpty @PathVariable List<Long> ids) {
log.info("delete users, param: ids={}", ids);
return RespResult.successData(ids.size());
}
//省略...
}
//=======================================================================================
//================================= Model对象层代码 ======================================
//=======================================================================================
/**
* 新增用户参数
*
* @author luohq
* @date 2022-01-15 12:21
*/
@Data
@Builder
@ApiModel("新增用户参数")
public class UserAddDto {
/**
* 用户名称
*/
@ApiModelProperty(value = "用户名称")
@NotBlank
@Size(min = 1, max = 30)
private String name;
/**
* 用户性别(1:男,2:女)
*/
@ApiModelProperty(value = "用户性别(1:男,2:女)")
@NotNull
@Range(min = 1, max = 2)
private Integer sex;
/**
* 设备列表
*/
@ApiModelProperty(value = "设备列表")
@NotEmpty
private List<DeviceAddDto> deviceInfoList;
}
添加Swagger 2.0相关注解完成后重启应用,Swagger-ui效果如下图:
maven依赖:
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.2version>
<relativePath/>
parent>
<properties>
<springfox3.version>3.0.0springfox3.version>
<swagger.version>1.6.4swagger.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-boot-starterartifactId>
<version>${springfox3.version}version>
dependency>
<dependency>
<groupId>io.swaggergroupId>
<artifactId>swagger-annotationsartifactId>
<version>${swagger.version}version>
dependency>
<dependency>
<groupId>io.swaggergroupId>
<artifactId>swagger-modelsartifactId>
<version>${swagger.version}version>
dependency>
dependencies>
应用配置application.yaml:
spring:
mvc:
pathmatch:
# 设置path匹配策略,解决springfox启动空指针异常问题,
# 具体参见:https://blog.csdn.net/Faint35799/article/details/122344731
matching-strategy: ant_path_matcher
代码配置:
import com.luo.demo.sc.base.enums.RespCodeEnum;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import springfox.documentation.builders.*;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Response;
import springfox.documentation.service.ResponseMessage;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Springfox配置
*
* @author luohq
* @date 2022-01-15 17:33
*/
@Configuration
public class SpringfoxConfig {
@Bean
public Docket createRestApi() {
List<Response> respMsgList = this.convertRespMsgList();
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.globalResponses(HttpMethod.GET, respMsgList)
.globalResponses(HttpMethod.POST, respMsgList)
.globalResponses(HttpMethod.PUT, respMsgList)
.globalResponses(HttpMethod.DELETE, respMsgList)
.select()
// 为当前包路径
.apis(RequestHandlerSelectors.basePackage("com.luo.demo.api.controller"))
.paths(PathSelectors.any())
.build();
}
@Bean
public ApiInfo apiInfo() {
return new ApiInfoBuilder()
// 页面标题
.title("Springfox OAS3.0 - RESTful API")
// 创建人信息
.contact(new Contact("luohq", "https://blog.csdn.net/luo15242208310", "[email protected]"))
// 版本号
.version("1.0")
// 描述
.description("Springfox OAS3.0 构建RESTful API" + this.convertRespMsgHtmlTable())
.build();
}
/**
* 转换响应码枚举RespCodeEnum为响应信息列表
*
* @return 响应信息列表
*/
private List<Response> convertRespMsgList() {
return Stream.of(RespCodeEnum.values())
.map(respCodeEnum -> {
return new ResponseBuilder()
.code(respCodeEnum.getCode().toString())
.description(respCodeEnum.getMessage())
.build();
}).collect(Collectors.toList());
}
/**
* 转换通用响应码Table
*
* @return 响应码Table
*/
private String convertRespMsgHtmlTable() {
StringBuilder sb = new StringBuilder("响应码 提示信息 ");
Stream.of(RespCodeEnum.values()).forEach(respCodeEnum -> {
sb.append("")
.append(respCodeEnum.getCode())
.append(" ")
.append(respCodeEnum.getMessage())
.append(" ");
});
return sb.append("
").toString();
}
}
添加OAS 3.0注解的示例代码如下:
//=======================================================================================
//================================ Controller层代码 ======================================
//=======================================================================================
@Tag(name = "用户信息管理")
@Slf4j
@RestController
@RequestMapping("/users")
@Validated
public class UserController {
private final Integer TOTAL_DEFAULT = 3;
private final Long ID_DEFAULT = 1L;
/**
* 查询用户详细信息
*
* @param id 用户ID
* @return 用户信息
*/
@Operation(summary = "查询用户信息", description = "根据用户ID查询用户信息",
responses = {
@ApiResponse(responseCode = "1000", description = "操作成功", content = @Content),
@ApiResponse(responseCode = "1101", description = "参数无效", content = @Content),
@ApiResponse(responseCode = "2000", description = "操作失败", content = @Content),
})
@GetMapping("/{id}")
public RespResult<UserInfo> getUser(@Parameter(name = "id", description = "用户ID", example = "1", in = ParameterIn.PATH, required = true)
@NotNull @PathVariable Long id) {
log.info("get user, param: id={}", id);
return RespResult.successData(this.buildUser(id));
}
/**
* 查询用户列表
*
* @param userQueryDto 查询参数
* @return 用户列表
*/
@Operation(summary = "查询用户信息列表", description = "查询用户信息列表",
parameters = {
@Parameter(name = "id", description = "用户ID", example = "1", in = ParameterIn.QUERY),
@Parameter(name = "name", description = "用户姓名", in = ParameterIn.QUERY),
@Parameter(name = "sex", description = "用户性别(1:男,2:女)", example = "1", in = ParameterIn.QUERY),
@Parameter(name = "createTimeStart", description = "起始创建日期", example = "2022-01-01 10:00:00", in = ParameterIn.QUERY),
@Parameter(name = "createTimeEnd", description = "结束创建日期", example = "2022-01-01 10:00:00", in = ParameterIn.QUERY),
},
responses = {
@ApiResponse(responseCode = "1000", description = "操作成功", content = @Content),
@ApiResponse(responseCode = "1101", description = "参数无效", content = @Content),
@ApiResponse(responseCode = "2000", description = "操作失败", content = @Content),
}
)
//@Operation(summary = "查询用户信息列表", description = "查询用户信息列表")
@GetMapping
public RespResult<UserInfo> getUsers(@Parameter(hidden = true) @Validated UserQueryDto userQueryDto) {
log.info("get users, param: {}", userQueryDto);
return RespResult.successRows(TOTAL_DEFAULT, this.buildUsers(Optional.ofNullable(userQueryDto.getId()).orElse(ID_DEFAULT), TOTAL_DEFAULT));
}
/**
* 新增用户及设备绑定信息
*
* @param userAddDto 新增参数
* @return 响应结果
*/
@Operation(summary = "新增用户及设备绑定信息",
responses = {
@ApiResponse(responseCode = "1000", description = "操作成功", content = @Content),
@ApiResponse(responseCode = "1101", description = "参数无效", content = @Content),
@ApiResponse(responseCode = "2000", description = "操作失败", content = @Content),
})
@PostMapping
public RespResult<Integer> addUser(@Validated @RequestBody UserAddDto userAddDto) {
log.info("add user, param: {}", userAddDto);
return RespResult.successData(1);
}
/**
* 修改用户及设备绑定信息
*
* @param userEditDto 修改参数
* @return 响应结果
*/
@Operation(summary = "修改用户及设备绑定信息",
responses = {
@ApiResponse(responseCode = "1000", description = "操作成功", content = @Content),
@ApiResponse(responseCode = "1101", description = "参数无效", content = @Content),
@ApiResponse(responseCode = "2000", description = "操作失败", content = @Content),
})
@PutMapping
public RespResult<Integer> updateUser(@Validated @RequestBody UserEditDto userEditDto) {
log.info("update user, param: {}", userEditDto);
return RespResult.successData(1);
}
/**
* 删除用户信息
*
* @param ids 用户ID列表
* @return 响应结果
*/
@Operation(summary = "删除用户信息", description = "根据用户ID列表删除用户信息",
responses = {
@ApiResponse(responseCode = "1000", description = "操作成功", content = @Content),
@ApiResponse(responseCode = "1101", description = "参数无效", content = @Content),
@ApiResponse(responseCode = "2000", description = "操作失败", content = @Content),
})
@DeleteMapping("/{ids}")
public RespResult<Integer> deleteUsers(@Parameter(name = "ids", description = "用户ID列表(逗号分隔)", in = ParameterIn.PATH, required = true)
@NotEmpty @PathVariable List<Long> ids) {
log.info("delete users, param: ids={}", ids);
return RespResult.successData(ids.size());
}
//省略...
}
//=======================================================================================
//================================ Model对象层代码 =======================================
//=======================================================================================
/**
* 新增用户参数
*
* @author luohq
* @date 2022-01-15 12:21
*/
@Data
@Builder
@Schema(description = "新增用户参数")
public class UserAddDto {
/**
* 用户名称
*/
@Schema(description = "用户名称")
@NotBlank
@Size(min = 1, max = 30)
private String name;
/**
* 用户性别(1:男,2:女)
*/
@Schema(description = "用户性别(1:男,2:女)", example = "1")
@NotNull
@Range(min = 1, max = 2)
private Integer sex;
/**
* 设备列表
*/
@Schema(description = "设备列表")
@NotEmpty
private List<DeviceAddDto> deviceInfoList;
}
启动项目,浏览器访问:http://localhost:8080/swagger-ui/
注: Springfox2和Springfox3版本的swagger-ui访问地址不同
maven依赖:
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.2version>
<relativePath/>
parent>
<properties>
<springdoc.version>1.6.4springdoc.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependencies>
<dependency>
<groupId>org.springdocgroupId>
<artifactId>springdoc-openapi-uiartifactId>
<version>${springdoc.version}version>
dependency>
dependencies>
dependencies>
应用配置application.yaml:
# springdoc配置
springdoc:
# 分组配置
group-configs:
- group: 用户管理
packages-to-scan: com.luo.demo.api.controller
paths-to-match: /users/**
- group: 角色管理
packages-to-scan: com.luo.demo.api.controller
paths-to-match: /roles/**
代码配置:
import com.luo.demo.sc.base.enums.RespCodeEnum;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.stream.Stream;
/**
* OAS 3.0 配置
*
* @author luohq
* @date 2022-01-16 12:34
*/
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI springShopOpenAPI() {
return new OpenAPI()
.info(new Info().title("Springdoc OAS3.0 - RESTful API")
.description("Springdoc OAS3.0 构建RESTful API" + this.convertRespMsgHtmlTable())
.version("1.0")
.license(new License().name("Apache 2.0").url("http://springdoc.org")))
.servers(Arrays.asList(
new Server().description("开发环境").url("http://localhost:8080")
))
.externalDocs(new ExternalDocumentation()
.description("SpringShop Wiki Documentation")
.url("https://springshop.wiki.github.org/docs"));
}
///**
// * 全局设置响应码(实际测试如果设置group(代码或配置文件)则此段代码不生效)
// *
// * @return
// */
//@Bean
//public OperationCustomizer customizeOperation() {
// return (operation, handlerMethod) -> {
// System.out.println("op: " + operation.getSummary());
// ApiResponses curResponses = operation.getResponses();
// Stream.of(RespCodeEnum.values()).forEach(respCodeEnum -> {
// curResponses.addApiResponse(
// String.valueOf(respCodeEnum.getCode()),
// new ApiResponse().description(respCodeEnum.getMessage()));
// });
// return operation.responses(curResponses);
// };
//}
//代码配置分组(亦可直接通过配置文件进行配置springdoc.group-configs[*])
//@Bean
//public GroupedOpenApi publicApi() {
// return GroupedOpenApi.builder()
// .group("用户管理")
// .pathsToMatch("/users/**")
// .build();
//}
//@Bean
//public GroupedOpenApi adminApi() {
// return GroupedOpenApi.builder()
// .group("角色管理")
// .pathsToMatch("/roles/**")
// .build();
//}
/**
* 转换通用响应码Table
*
* @return 响应码Table
*/
private String convertRespMsgHtmlTable() {
StringBuilder sb = new StringBuilder("响应码 提示信息 ");
Stream.of(RespCodeEnum.values()).forEach(respCodeEnum -> {
sb.append("")
.append(respCodeEnum.getCode())
.append(" ")
.append(respCodeEnum.getMessage())
.append(" ");
});
return sb.append("
").toString();
}
}
添加OAS 3.0注解的示例代码如下(同之前Springfox3中使注解相同):
//=======================================================================================
//================================ Controller层代码 ======================================
//=======================================================================================
@Tag(name = "用户信息管理")
@Slf4j
@RestController
@RequestMapping("/users")
@Validated
public class UserController {
private final Integer TOTAL_DEFAULT = 3;
private final Long ID_DEFAULT = 1L;
/**
* 查询用户详细信息
*
* @param id 用户ID
* @return 用户信息
*/
@Operation(summary = "查询用户信息", description = "根据用户ID查询用户信息",
responses = {
@ApiResponse(responseCode = "1000", description = "操作成功", content = @Content),
@ApiResponse(responseCode = "1101", description = "参数无效", content = @Content),
@ApiResponse(responseCode = "2000", description = "操作失败", content = @Content),
})
@GetMapping("/{id}")
public RespResult<UserInfo> getUser(@Parameter(name = "id", description = "用户ID", example = "1", in = ParameterIn.PATH, required = true)
@NotNull @PathVariable Long id) {
log.info("get user, param: id={}", id);
return RespResult.successData(this.buildUser(id));
}
/**
* 查询用户列表
*
* @param userQueryDto 查询参数
* @return 用户列表
*/
@Operation(summary = "查询用户信息列表", description = "查询用户信息列表",
parameters = {
@Parameter(name = "id", description = "用户ID", example = "1", in = ParameterIn.QUERY),
@Parameter(name = "name", description = "用户姓名", in = ParameterIn.QUERY),
@Parameter(name = "sex", description = "用户性别(1:男,2:女)", example = "1", in = ParameterIn.QUERY),
@Parameter(name = "createTimeStart", description = "起始创建日期", example = "2022-01-01 10:00:00", in = ParameterIn.QUERY),
@Parameter(name = "createTimeEnd", description = "结束创建日期", example = "2022-01-01 10:00:00", in = ParameterIn.QUERY),
},
responses = {
@ApiResponse(responseCode = "1000", description = "操作成功", content = @Content),
@ApiResponse(responseCode = "1101", description = "参数无效", content = @Content),
@ApiResponse(responseCode = "2000", description = "操作失败", content = @Content),
}
)
//@Operation(summary = "查询用户信息列表", description = "查询用户信息列表")
@GetMapping
public RespResult<UserInfo> getUsers(@Parameter(hidden = true) @Validated UserQueryDto userQueryDto) {
log.info("get users, param: {}", userQueryDto);
return RespResult.successRows(TOTAL_DEFAULT, this.buildUsers(Optional.ofNullable(userQueryDto.getId()).orElse(ID_DEFAULT), TOTAL_DEFAULT));
}
/**
* 新增用户及设备绑定信息
*
* @param userAddDto 新增参数
* @return 响应结果
*/
@Operation(summary = "新增用户及设备绑定信息",
responses = {
@ApiResponse(responseCode = "1000", description = "操作成功", content = @Content),
@ApiResponse(responseCode = "1101", description = "参数无效", content = @Content),
@ApiResponse(responseCode = "2000", description = "操作失败", content = @Content),
})
@PostMapping
public RespResult<Integer> addUser(@Validated @RequestBody UserAddDto userAddDto) {
log.info("add user, param: {}", userAddDto);
return RespResult.successData(1);
}
/**
* 修改用户及设备绑定信息
*
* @param userEditDto 修改参数
* @return 响应结果
*/
@Operation(summary = "修改用户及设备绑定信息",
responses = {
@ApiResponse(responseCode = "1000", description = "操作成功", content = @Content),
@ApiResponse(responseCode = "1101", description = "参数无效", content = @Content),
@ApiResponse(responseCode = "2000", description = "操作失败", content = @Content),
})
@PutMapping
public RespResult<Integer> updateUser(@Validated @RequestBody UserEditDto userEditDto) {
log.info("update user, param: {}", userEditDto);
return RespResult.successData(1);
}
/**
* 删除用户信息
*
* @param ids 用户ID列表
* @return 响应结果
*/
@Operation(summary = "删除用户信息", description = "根据用户ID列表删除用户信息",
responses = {
@ApiResponse(responseCode = "1000", description = "操作成功", content = @Content),
@ApiResponse(responseCode = "1101", description = "参数无效", content = @Content),
@ApiResponse(responseCode = "2000", description = "操作失败", content = @Content),
})
@DeleteMapping("/{ids}")
public RespResult<Integer> deleteUsers(@Parameter(name = "ids", description = "用户ID列表(逗号分隔)", in = ParameterIn.PATH, required = true)
@NotEmpty @PathVariable List<Long> ids) {
log.info("delete users, param: ids={}", ids);
return RespResult.successData(ids.size());
}
//省略...
}
//=======================================================================================
//================================ Model对象层代码 =======================================
//=======================================================================================
/**
* 新增用户
*
* @author luohq
* @date 2022-01-15 12:21
*/
@Data
@Builder
@Schema(description = "新增用户参数")
public class UserAddDto {
/**
* 用户名称
*/
@Schema(description = "用户名称")
@NotBlank
@Size(min = 1, max = 30)
private String name;
/**
* 用户性别(1:男,2:女)
*/
@Schema(description = "用户性别(1:男,2:女)", example = "1")
@NotNull
@Range(min = 1, max = 2)
private Integer sex;
/**
* 设备列表
*/
@Schema(description = "设备列表")
@NotEmpty
private List<DeviceAddDto> deviceInfoList;
}
启动项目,浏览器访问:http://localhost:8080/swagger-ui.html
注:
smart-doc提供maven(或gradle)插件,通过集成插件运行mvn相关命令即可生成文档。
示例命令如下:
# 生成 Open Api 3.0+,Since smart-doc-maven-plugin 1.1.5
mvn -Dfile.encoding=UTF-8 smart-doc:openapi
# 生成html
mvn -Dfile.encoding=UTF-8 smart-doc:html
# 生成markdown
mvn -Dfile.encoding=UTF-8 smart-doc:markdown
# 生成adoc
mvn -Dfile.encoding=UTF-8 smart-doc:adoc
# 生成postman json数据
mvn -Dfile.encoding=UTF-8 smart-doc:postman
# 生成文档推送到Torna平台
mvn -Dfile.encoding=UTF-8 smart-doc:torna-rest
maven依赖:
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.2version>
<relativePath/>
parent>
<properties>
<springdoc.version>1.6.4springdoc.version>
<smart.doc.version>2.3.6smart.doc.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springdocgroupId>
<artifactId>springdoc-openapi-uiartifactId>
<version>${springdoc.openapi.version}version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>com.github.shalousungroupId>
<artifactId>smart-doc-maven-pluginartifactId>
<version>${smart.doc.version}version>
<configuration>
<configFile>./src/main/resources/smart-doc.jsonconfigFile>
<excludes>
<exclude>com.alibaba:.*exclude>
<exclude>cn.hutool:hutool-coreexclude>
excludes>
configuration>
<executions>
<execution>
<phase>compilephase>
<goals>
<goal>openapigoal>
goals>
execution>
executions>
plugin>
plugins>
build>
smart-doc.json配置:
注:
./src/main/resources/smart-doc.json
即对应resources/smart-doc.json文件。{
"projectName": "Smartdoc + springdoc + OAS3.0 - RESTful API",
"serverUrl": "http://localhost:8080",
"pathPrefix": "/",
"outPath": "./src/main/resources/static/doc",
"allInOne": true,
"showAuthor": true,
"groups": [
{
"name": "用户管理",
"apis": "com.luo.demo.api.controller.user.*"
},
{
"name": "角色管理",
"apis": "com.luo.demo.api.controller.role.*"
}
],
"revisionLogs": [
{
"version": "1.0",
"revisionTime": "2022-01-17 16:30",
"status": "create",
"author": "luohq",
"remarks": "Smartdoc OAS3集成Springdoc"
}
]
}
应用配置application.yaml:
# custom path for swagger-ui
springdoc:
swagger-ui:
# 自定义openapi.json文件位置(需在/resources/static目录下)
url: /doc/openapi.json
代码配置:
无
示例代码如下(仅添加标准的Java注释即可):
//=======================================================================================
//================================ Controller层代码 ======================================
//=======================================================================================
/**
* 用户信息管理
*
* @author luohq
* @apiNote 用户信息管理
* @date 2022-01-15 12:29
*/
@Slf4j
@RestController
@RequestMapping("/users")
@Validated
public class UserController {
private final Integer TOTAL_DEFAULT = 3;
private final Long ID_DEFAULT = 1L;
/**
* 查询用户信息
*
* @param id 用户ID
* @return 用户信息
* @apiNote 根据用户ID查询用户信息
*/
@GetMapping("/{id}")
public RespResult<UserInfo> getUser(@NotNull @PathVariable Long id) {
log.info("get user, param: id={}", id);
return RespResult.successData(this.buildUser(id));
}
/**
* 查询用户信息列表
*
* @param userQueryDto 查询参数
* @return 用户列表
*/
@GetMapping
public RespResult<UserInfo> getUsers(@Validated UserQueryDto userQueryDto) {
log.info("get users, param: {}", userQueryDto);
return RespResult.successRows(TOTAL_DEFAULT, this.buildUsers(Optional.ofNullable(userQueryDto.getId()).orElse(ID_DEFAULT), TOTAL_DEFAULT));
}
/**
* 新增用户及设备绑定信息
*
* @param userAddDto 新增参数
* @return 响应结果
*/
@PostMapping
public RespResult<Integer> addUser(@Validated @RequestBody UserAddDto userAddDto) {
log.info("add user, param: {}", userAddDto);
return RespResult.successData(1);
}
/**
* 修改用户及设备绑定信息
*
* @param userEditDto 修改参数
* @return 响应结果
*/
@PutMapping
public RespResult<Integer> updateUser(@Validated @RequestBody UserEditDto userEditDto) {
log.info("update user, param: {}", userEditDto);
return RespResult.successData(1);
}
/**
* 删除用户信息
*
* @param ids 用户ID列表
* @return 响应结果
* @apiNote 根据用户ID列表删除用户信息
*/
@DeleteMapping("/{ids}")
public RespResult<Integer> deleteUsers(@NotEmpty @PathVariable List<Long> ids) {
log.info("delete users, param: ids={}", ids);
return RespResult.successData(ids.size());
}
//省略...
}
//=======================================================================================
//================================ Model对象层代码 =======================================
//=======================================================================================
/**
* 新增用户参数
*
* @author luohq
* @date 2022-01-15 12:21
*/
@Data
@Builder
public class UserAddDto {
/**
* 用户名称
*/
@NotBlank
@Size(min = 1, max = 30)
private String name;
/**
* 用户性别(1:男,2:女)
*/
@NotNull
@Range(min = 1, max = 2)
private Integer sex;
/**
* 设备列表
*/
@NotEmpty
private List<DeviceAddDto> deviceInfoList;
}
之前集成Springfox、Springdoc皆是在程序运行时扫描代码和Swagger相关注解后生成的Api文档,
而Smart-doc可以在编译期(mvn compile)或者程序启动前通过插件命令预先生成Api文档,
即可依次通过如下几步进行集成和启动:
mvn -Dfile.encoding=UTF-8 smart-doc:openapi
./src/main/resources/static/doc/openapi.json
springdoc.swagger-ui.url
去引用此openapi.json
springdoc.swagger-ui.url=/doc/openapi.json
启动项目,浏览器访问:http://localhost:8080/swagger-ui.html (对应Springdoc OAS3 Swagger-ui界面),
启动后Swagger-ui效果如下图:
除了使用swagger-ui渲染,还发现了国内开源的Knife4j,
Knife4j提供了一个swagger-ui增强界面(还提供其他增强功能,本文暂未涉及),
通过Disk本地模式聚合OpenAPI文档模式,
即通过Knife4j去渲染之前生成的openapi.json文件,
实际测试后发现其对OAS 3.0的解析及渲染还不是很完善,如对象嵌套的结构皆显示不出来
,
开源不易,还是得感谢大神,希望Knife4j可以越来越好。
关于Knife4j聚合本地文件openapi.json的具体集成见下文。
maven配置:
<properties>
<knife4j.aggregation.version>2.0.9knife4j.aggregation.version>
properties>
<dependencies>
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>knife4j-aggregation-spring-boot-starterartifactId>
<version>${knife4j.aggregation.version}version>
dependency>
dependencies>
应用配置application.yaml:
# knife4j聚合配置
knife4j:
enableAggregation: true
disk:
enable: true
routes:
- name: 用户
location: classpath:static/doc/openapi.json
启动后即可通过http://localhost:8080/doc.html进行访问,
实际集成后发现其对OAS 3.0的解析及渲染还不是很完善,如对象嵌套的结构皆显示不出来,如下图:
在springdoc生态中提供了一个Javadoc模块 - springdoc-openapi-javadoc
,
该模块增强了springdoc对Java注释及Tag的处理能力,
但支持有限,目前仅支持:
Smart-doc相较于Springdoc-openapi-javadoc的源码模式,Smart-doc额外支持:
@Operation.summary
(此处缺失可参见下文图片效果)@apiNote
注释为@Operation.description
mock值
(@param desc|mockVal、@mock mockVal)Springdoc-openapi-javadoc除了支持在运行时获取代码注释信息,
也可配合springdoc生态提供的maven插件 - springdoc-openapi-maven-plugin
使用,
该插件可在mvn verify
阶段扫描源码来生成对应的openapi.json
文件。
具体springdoc-openapi-javadoc和springdoc-openapi-maven-plugin的集成及使用见下文。
maven依赖:
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.2version>
<relativePath/>
parent>
<properties>
<springdoc.version>1.6.4springdoc.version>
<springdoc.plugin.version>1.3springdoc.plugin.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springdocgroupId>
<artifactId>springdoc-openapi-uiartifactId>
<version>${springdoc.version}version>
dependency>
<dependency>
<groupId>org.springdocgroupId>
<artifactId>springdoc-openapi-javadocartifactId>
<version>${springdoc.version}version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.github.therapigroupId>
<artifactId>therapi-runtime-javadoc-scribeartifactId>
<version>0.13.0version>
path>
annotationProcessorPaths>
configuration>
plugin>
<plugin>
<groupId>org.springdocgroupId>
<artifactId>springdoc-openapi-maven-pluginartifactId>
<version>${springdoc.plugin.version}version>
<executions>
<execution>
<id>integration-testid>
<goals>
<goal>generategoal>
goals>
execution>
executions>
<configuration>
<apiDocsUrl>http://localhost:8080/v3/api-docsapiDocsUrl>
<outputFileName>openapi.jsonoutputFileName>
<outputDir> ${project.build.directory}outputDir>
<skip>falseskip>
configuration>
plugin>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<jvmArguments>-Dspring.application.admin.enabled=truejvmArguments>
configuration>
<executions>
<execution>
<id>pre-integration-testid>
<goals>
<goal>startgoal>
goals>
execution>
<execution>
<id>post-integration-testid>
<goals>
<goal>stopgoal>
goals>
execution>
executions>
plugin>
plugins>
build>
代码配置:
无
示例代码同smart-doc示例(仅添加标准的Java注释即可)
//=======================================================================================
//================================ Controller层代码 ======================================
//=======================================================================================
/**
* 用户信息管理
*
* @author luohq
* @apiNote 用户信息管理
* @date 2022-01-15 12:29
*/
@Slf4j
@RestController
@RequestMapping("/users")
@Validated
public class UserController {
private final Integer TOTAL_DEFAULT = 3;
private final Long ID_DEFAULT = 1L;
/**
* 查询用户信息
*
* @param id 用户ID
* @return 用户信息
* @apiNote 根据用户ID查询用户信息
*/
@GetMapping("/{id}")
public RespResult<UserInfo> getUser(@NotNull @PathVariable Long id) {
log.info("get user, param: id={}", id);
return RespResult.successData(this.buildUser(id));
}
/**
* 查询用户信息列表
*
* @param userQueryDto 查询参数
* @return 用户列表
*/
@GetMapping
public RespResult<UserInfo> getUsers(@Validated UserQueryDto userQueryDto) {
log.info("get users, param: {}", userQueryDto);
return RespResult.successRows(TOTAL_DEFAULT, this.buildUsers(Optional.ofNullable(userQueryDto.getId()).orElse(ID_DEFAULT), TOTAL_DEFAULT));
}
/**
* 新增用户及设备绑定信息
*
* @param userAddDto 新增参数
* @return 响应结果
*/
@PostMapping
public RespResult<Integer> addUser(@Validated @RequestBody UserAddDto userAddDto) {
log.info("add user, param: {}", userAddDto);
return RespResult.successData(1);
}
/**
* 修改用户及设备绑定信息
*
* @param userEditDto 修改参数
* @return 响应结果
*/
@PutMapping
public RespResult<Integer> updateUser(@Validated @RequestBody UserEditDto userEditDto) {
log.info("update user, param: {}", userEditDto);
return RespResult.successData(1);
}
/**
* 删除用户信息
*
* @param ids 用户ID列表
* @return 响应结果
* @apiNote 根据用户ID列表删除用户信息
*/
@DeleteMapping("/{ids}")
public RespResult<Integer> deleteUsers(@NotEmpty @PathVariable List<Long> ids) {
log.info("delete users, param: ids={}", ids);
return RespResult.successData(ids.size());
}
//省略...
}
//=======================================================================================
//================================ Model对象层代码 =======================================
//=======================================================================================
/**
* 新增用户参数
*
* @author luohq
* @date 2022-01-15 12:21
*/
@Data
@Builder
public class UserAddDto {
/**
* 用户名称
*/
@NotBlank
@Size(min = 1, max = 30)
private String name;
/**
* 用户性别(1:男,2:女)
*/
@NotNull
@Range(min = 1, max = 2)
private Integer sex;
/**
* 设备列表
*/
@NotEmpty
private List<DeviceAddDto> deviceInfoList;
}
启动方式一:
- 可以直接启动项目,
- 浏览器访问:http://localhost:8080/swagger-ui.html,
启动方式二:
- 执行mvn verify命令,生成openapi.json到target目录下,
- 拷贝target/openapi.json到应用代码目录resources/static/doc/openapi.json
- 添加application.yaml配置springdoc.swagger-ui.url=/doc/openapi.json(可参见前文smart-doc结合springdoc展示openapi.json)
- 然后再启动项目,浏览器访问:http://localhost:8080/swagger-ui.html
以上两种启动方式的最终展示效果都是一样的,
具体Swagger-ui界面如下图(左边为smart-doc,右边为springdoc javadoc
,可以对比着看):
综合对比springdoc-openapi-javadoc和smart-doc,现阶段我还是更倾向于smart-doc,主要原因如下:
参考链接:
swagger:
https://swagger.io/
openapi:
OAS2.0 & 3.0 规范 - https://swagger.io/resources/open-api/
OAS3.0规范 - https://swagger.io/specification/
springfox:
http://springfox.github.io/springfox/
http://springfox.github.io/springfox/docs/current/
从Springfox2迁移到SpringDoc - https://springdoc.org/#migrating-from-springfox
springdoc:
https://springdoc.org/
https://github.com/springdoc/springdoc-openapi-maven-plugin
springdoc设置全局response:
https://github.com/springdoc/springdoc-openapi/issues/114
https://github.com/springdoc/springdoc-openapi/issues/381
https://stackoverflow.com/questions/60869480/default-response-class-in-springdoc
springdoc maven plugin 404报错:
https://stackoverflow.com/questions/59616165/what-is-the-function-of-springdoc-openapi-maven-plugin-configuration-apidocsurl
smart-doc:
https://smart-doc-group.github.io/
其他:
Spring Boot 中使用 SpringFox 整合 Swagger 3(OpenAPI 3)生成 API 文档
Spring Boot 中使用 SpringDoc 整合 Swagger 3(OpenAPI 3)生成 API 文档
https://www.baeldung.com/spring-rest-openapi-documentation