参考原文链接:https://blog.csdn.net/Mrqiang9001/article/details/132305015
1 摘要
Swagger 作为一款服务端接口文档自动生成框架,早已深入人心,并且在市场上得到了广泛的应用。然而,Swagger 3.0 也就是 OpenApi 3.0 规范发布之后便停止了更新维护,出道就是巅峰。Knife4j 作为 Swagger 的增强版,是对 Swagger UI 做了优化,同时还有很多增强的功能。伴随着 Swagger 3.0 的停止更新,如今 Knife4j 从4.0开始已经逐渐使用 Springdoc 作为 swagger 的替代。Springdoc 针对 OpenApi 3.0 的适配做了较大的调整,其中注解与 Swagger 2 的基本不通用。作为新项目而言,使用社区维护活跃的开源框架显得非常重要。本文将介绍基于 Springboot 2.7 集成 Swagger 的增强框架 Knife4j 4.3 + Springdoc OpenApi 3.0 。
Knife4j 官方文档: https://doc.xiaominfo.com/docs/quick-start
Springdoc 官方文档: https://springdoc.org
SpringDoc注解解析:https://blog.csdn.net/qq_29012499/article/details/135433483
解决接口模块儿排序问题:knife4j、swagger、springdoc 返回接口分组排序问题 https://blog.csdn.net/neusoft2016/article/details/130975683
核心 Maven 依赖
<!-- mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!-- knife4j openapi3 接口文档 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
核心代码
application.yml
# spring-doc 接口文档
springdoc:
api-docs:
enabled: true # 是否启用接口文档
knife4j:
enable: true # 是否启用 knife4j 增强,如果只是使用 knife4j 的 UI,则可以关闭
openApi 配置类
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.GroupedOpenApi;
import org.springdoc.core.customizers.OpenApiCustomiser;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Comparator;
import java.util.stream.Collectors;
@OpenAPIDefinition(
security = @SecurityRequirement(name = "Authorization")
)
@SecurityScheme(type = SecuritySchemeType.APIKEY, name = "Authorization", scheme = "Authorization", in = SecuritySchemeIn.HEADER)
@Configuration
public class OpenApiConfig {
private String title = "SpringDoc API";
private String description = "Knife4j OpenApi 3 description";
private String version = "v0.0.1";
@Bean
public OpenAPI springOpenAPI() {
return new OpenAPI()
.info(new Info()
.title(title)
.description(description)
.version(version));
}
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group(title)
.pathsToMatch("/**")
.addOpenApiCustomiser(sortTagsAlphabetically())
.build();
}
//排序规则方法
public OpenApiCustomiser sortTagsAlphabetically() {
return openApi -> openApi.setTags(openApi.getTags()
.stream()
.sorted(Comparator.comparing(tag -> StringUtils.stripAccents(tag.getName())))
.collect(Collectors.toList()));
}
}
DTO
package com.sunmon.turtle.domain.dto;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.ToString;
import javax.validation.constraints.NotNull;
@Data
@ToString(callSuper = true)
@Schema(description = "用户表单实体")
@TableName(value = "user")
public class UserFormDTO {
@NotNull(message = "注册手机号 不能为空")
@Schema(description = "注册手机号", requiredMode = Schema.RequiredMode.REQUIRED)
private String phone;
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED)
private String userName;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED)
private String password;
@Schema(description = "用户id")
private Long id;
@Schema(description = "详细信息,JSON风格")
private String info;
@Schema(description = "账户余额")
private Integer balance;
}
PO
package com.sunmon.turtle.domain.po;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.sunmon.turtle.utils.MyLocalDateTypeHandler;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Data
@Component
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User {
// @TableId//不指定 IdType 时,默认使用雪花算法生成数字(企业级开发时使用)
@TableId(type= IdType.AUTO)//自增
private Long id;
private String userName;
private String password;
private String phone;
private String gender;
private String info;
private String loginId;
private Integer status;//用户状态(1启用 0停用)
@TableField(exist = false)
private String address;
private Integer balance;
//使用自定义处理器,解决 “mybatis-plus 在映射查询结果到 Java 对象时,如果LocalDateTime为空时,引发 NullPointerException”
@TableField( typeHandler = MyLocalDateTypeHandler.class)
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime regDate;
@TableField( typeHandler = MyLocalDateTypeHandler.class, fill = FieldFill.INSERT)
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@TableField( typeHandler = MyLocalDateTypeHandler.class, fill = FieldFill.UPDATE)
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}
@Schema 注解中参数是否必填使用了一个枚举类 io.swagger.v3.oas.annotations.media.Schema.RequiredMode 这是 @Schema 注解的一个内部类,支持 3 种模式, @Schema 注解中多种属性也采用了枚举类的方式,这是与原来的 @ApiModelPropertity 注解区别比较大的地方。
VO
package com.sunmon.turtle.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.ToString;
import java.time.LocalDateTime;
@Data
@ToString(callSuper = true)
@Schema(description = "用户 VO 实体")
public class UserVO {
@Schema(description = "用户id")
private Long id;
@Schema(description = "用户名")
private String userName;
@Schema(description = "详细信息")
private String info;
@Schema(description = "用户状态(1启用 0停用)")
private String status;
@Schema(description = "账户余额")
private Integer balance;
@Schema(description = "注册时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")//解决 MySQL日期前端显示问题——数据转换出来后多个 “T“
private LocalDateTime regDate;
@Schema(description = "创建时间")//, type = "dateTime",pattern = "yyyy-MM-dd HH:mm:ss"
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
@Schema(description = "最后更新时间")//, type = "dateTime",pattern = "yyyy-MM-dd HH:mm:ss"
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
}
Controller 控制层示例
package com.sunmon.turtle.controller;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.sunmon.turtle.domain.dto.UserFormDTO;
import com.sunmon.turtle.domain.query.UserQuery;
import com.sunmon.turtle.domain.vo.UserVO;
import com.sunmon.turtle.domain.po.User;
import com.sunmon.turtle.service.IUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@DS("db_turtle")
@Tag(name = "1 - 用户管理接口", description = "用户管理")
@RequestMapping("/users")
@RequiredArgsConstructor//构造时,用必要的参数生成构造函数
@RestController
public class UserController {
private final IUserService userService;
@Operation(summary = "01 用户新增接口")
@PostMapping
public boolean saveUser(@RequestBody UserFormDTO userDTO) {
//1.把 DTO拷贝到PO
User user = BeanUtil.copyProperties(userDTO, User.class);//HuTool工具包
//2.新增
return userService.save(user);
}
@Operation(summary = "02 根据 id 查询用户接口",
parameters = {
@Parameter( name = "用户id", required = true, in = ParameterIn.PATH, schema = @Schema(type = "integer") )
} )
@GetMapping("{id}")
public UserVO queryUserById ( @PathVariable("id") Long id) {
User user = userService.getById(id);
UserVO userVo = BeanUtil.copyProperties(user, UserVO.class);//HuTool工具包
return userVo;
}
@Operation(summary = "03 删除用户接口",
parameters = {
@Parameter(name = "用户id", required = true, in = ParameterIn.PATH)
}
)
@DeleteMapping("{id}")
public boolean deleteUserById( @PathVariable("id") Long id) {
return userService.removeById(id);
}
@Operation(summary = "05 扣减用户余额接口",
parameters = {
@Parameter(name = "用户id", required = true, in = ParameterIn.PATH),
@Parameter(name = "扣减的金额", required = true, in = ParameterIn.PATH)
}
)
@PutMapping("/{id}/deduction/{money}")
public void deductBalance( @PathVariable("id") Long id, @PathVariable("money") Integer money) {
userService.deductBalance(id, money);
}
@Operation(summary = "04 根据 id 批量查询用户接口" ,
parameters = {
@Parameter(name = "用户id集合", required = true, in = ParameterIn.PATH, schema = @Schema(type = "list") )
} )
@GetMapping
public List<UserVO> queryUserByIds( @RequestParam("ids") List<Long> ids) {
List<User> users = userService.listByIds(ids);
return BeanUtil.copyToList(users, UserVO.class);//HuTool工具包
}
@Operation(
summary = "06 根据复杂条件查询用户接口"
)
//在SpringDoc中,如果你遇到了Get请求带有RequestBody注解,并且有自定义参数,但是没有参数框的问题,这通常是因为Swagger UI的一个已知限制,它不支持为GET请求显示请求体参数。
//一种方法是使用POST请求替代GET请求,尽管这可能不符合RESTful API设计的原则。
@PostMapping("/list")
public List<UserVO> queryUsers( @RequestBody UserQuery query){
//1.查询用户
List<User> users = userService.queryUsers(
query.getName(),query.getStatus(),query.getMaxBalance(),query.getMinBalance()
);
//2.把PO 拷贝 VO
return BeanUtil.copyToList(users, UserVO.class);
}
}
在 Controller 类中,原来 Swagger 2 的 @Api 注解替换为 @Tag, 原来的 @ApiOperation 注解替换为 @Operation。
在请求头中的参数,例如 GET 请求,如果是一个对象,则需要使用 @ParameterObject 注解,如果不添加,则无法在 Swagger 接口文档界面进行正常请求。