最近在将项目从SpringBoot 2.7.18升级到3.3.0时,遇到了Knife4j的兼容性问题。由于Spring Boot 3.x系列只支持OpenAPI 3规范,原有的Knife4j 3.0.3版本已不再适用,必须升级到4.5.0版本。这个升级过程涉及到依赖变更、配置调整、注解更新等多个方面,下面我将详细记录整个升级过程,希望能帮助到有同样需求的开发者。
在开始之前,我们需要确认开发环境的基本配置:
提示:如果你是从Spring Boot 2.x升级到3.x,需要注意Jakarta EE 9带来的包名变更(javax.* → jakarta.*),这会影响很多依赖和注解。
首先需要在pom.xml中更新父项目和Knife4j依赖:
xml复制<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<relativePath/>
</parent>
<dependencies>
<!-- 其他依赖 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
</dependencies>
关键变化点:
knife4j-spring-boot-starter变为knife4j-openapi3-jakarta-spring-boot-starter在application.yml中需要配置springdoc和knife4j相关参数:
yaml复制# Knife4j配置
springdoc:
default-flat-param-object: true # GET请求多参数时不需要额外注解
swagger-ui:
path: /swagger-ui.html # UI访问路径
enabled: true
api-docs:
path: /v3/api-docs # 后端接口文档路径
enabled: true
knife4j:
enable: true # 开启Knife4j增强功能
setting:
language: ZH_CN # 中文界面
enable-swagger-models: true
enable-dynamic-parameter: false
footer-custom-content: "<strong>Copyright © 2024 Keyidea. All Rights Reversed</strong>"
enable-footer-custom: true
enable-footer: true
enable-document-manage: true
documents: # 附加文档配置
- name: MarkDown语法说明
locations: classpath:static/markdown/grammar/*
group: 01-系统接口
- name: 补充文档
locations: classpath:static/markdown/others/*
group: 01-系统接口
注意事项:
src/main/resources/static/markdown/下创建Knife4jConfig配置类,定义接口分组和全局设置:
java复制@Configuration
public class Knife4jConfig {
private static final String API_INFO_TITLE = "软件接口文档";
private static final String API_INFO_VERSION = "V1.0";
// 系统接口分组
@Bean
public GroupedOpenApi api1() {
return GroupedOpenApi.builder()
.group("01-sys-api")
.displayName("01-系统接口")
.packagesToScan("cn.keyidea.sys")
.addOpenApiCustomizer(this::setCustomStatusCode)
.build();
}
// 业务接口分组
@Bean
public GroupedOpenApi api2() {
return GroupedOpenApi.builder()
.group("02-business-api")
.displayName("02-业务接口")
.packagesToScan("cn.keyidea.business")
.addOpenApiCustomizer(this::setCustomStatusCode)
.build();
}
// OpenAPI基本信息配置
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title(API_INFO_TITLE)
.version(API_INFO_VERSION)
.contact(new Contact().name("Keyidea").email("support@keyidea.cn"))
.license(new License().name("2024年度内部文档")));
}
// 自定义状态码处理
private void setCustomStatusCode(OpenAPI openApi) {
if (openApi.getPaths() != null) {
openApi.getPaths().forEach((key, pathItem) -> {
processOperation(pathItem.getGet());
processOperation(pathItem.getPost());
processOperation(pathItem.getPut());
processOperation(pathItem.getDelete());
});
}
}
private void processOperation(Operation operation) {
if (operation != null) {
ApiResponses responses = operation.getResponses();
if (responses != null) {
Content content = responses.values().stream()
.findFirst()
.map(ApiResponse::getContent)
.orElse(new Content());
StatusCode.toMap().forEach((code, desc) -> {
responses.addApiResponse(String.valueOf(code),
new ApiResponse().description(desc).content(content));
});
}
}
}
}
关键点说明:
GroupedOpenApi定义接口分组,支持按包扫描displayName设置中文分组名(直接使用中文group名会导致访问异常)addOpenApiCustomizer用于添加自定义处理逻辑(如全局状态码)如果项目使用了Shiro等安全框架,需要放行Knife4j相关路径:
java复制// Shiro配置类中
filterMap.put("/doc.html", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/v3/**", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/swagger-ui/**", "anon");
与Swagger2的区别:
/v2/**变为/v3/**/swagger-ui/**路径从Swagger2到OpenAPI3,注解发生了较大变化:
| Swagger2注解 | OpenAPI3注解 | 说明 |
|---|---|---|
| @Api | @Tag | 控制器描述 |
| @ApiOperation | @Operation | 接口方法描述 |
| @ApiModel | @Schema | 模型类描述 |
| @ApiModelProperty | @Schema | 模型属性描述 |
| @ApiParam | @Parameter | 参数描述 |
| @ApiImplicitParams | @Parameters | 参数组描述 |
| @ApiResponse | @ApiResponse | 响应描述 |
java复制@ApiSupport(order = 1)
@Tag(name = "1-系统公共类", description = "系统公共类")
@RestController
@RequestMapping("/sys/common/")
public class CommonController {
@Operation(summary = "文件上传", description = "单文件上传接口")
@Parameters({
@Parameter(name = "file", description = "上传文件",
required = true, schema = @Schema(type = "file")),
@Parameter(name = "fileType", description = "文件类型", example = "txt")
})
@PostMapping("uploadFile")
public BaseRes uploadFile(@RequestPart MultipartFile file,
@RequestParam String fileType) {
// 实现逻辑
}
}
java复制@Schema(name = "PageObject", description = "分页参数对象")
public class PageObject {
@Schema(description = "当前页", example = "1", requiredMode = REQUIRED)
private Integer page;
@Schema(description = "分页大小", example = "15", requiredMode = REQUIRED)
private Integer pageSize;
}
java复制@ApiSupport(order = 2)
@Tag(name = "2-卫星TLE管理", description = "卫星两行根数管理")
@RestController
@RequestMapping("/v1/tle")
public class TleController {
@Operation(summary = "分页查询")
@Parameters({
@Parameter(name = "sceneId", description = "场景ID", example = "1"),
@Parameter(name = "tleCode", description = "节点标识", example = "0101")
})
@GetMapping("listPage")
public BaseRes<Page<Tle>> listPage(
@RequestParam(required = false) Integer sceneId,
@RequestParam(required = false) String tleCode,
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "15") Integer pageSize) {
// 实现逻辑
}
}
问题描述:使用tags-sorter: alpha排序时,数字前缀排序异常(如10排在1前面)
解决方案:
@ApiSupport(order = x)注解@Tag的description不为空问题描述:直接在group中使用中文会导致页面访问异常
解决方案:
问题描述:GET请求接收对象参数时,文档显示不正确
解决方案:
springdoc.default-flat-param-object: true@ModelAttribute注解问题描述:响应示例中的字段说明不显示
解决方案:
点击右上角"显示说明"按钮,触发一次显示/隐藏切换
通过配置类的addOpenApiCustomizer方法可以添加全局状态码:
java复制// 在Knife4jConfig类中
private void setCustomStatusCode(OpenAPI openApi) {
Map<Integer, String> statusCodes = Map.of(
1000, "请求成功",
1001, "非法字段",
2001, "TOKEN失效"
);
openApi.getPaths().forEach((path, item) -> {
processOperation(item.getGet(), statusCodes);
processOperation(item.getPost(), statusCodes);
// 其他HTTP方法...
});
}
private void processOperation(Operation op, Map<Integer, String> statusCodes) {
if (op != null) {
ApiResponses responses = op.getResponses();
Content content = responses.values().stream()
.findFirst()
.map(ApiResponse::getContent)
.orElse(new Content());
statusCodes.forEach((code, desc) -> {
responses.addApiResponse(String.valueOf(code),
new ApiResponse().description(desc).content(content));
});
}
}
对于文件上传接口,推荐以下配置:
java复制@Operation(summary = "文件上传")
@Parameters({
@Parameter(name = "file", description = "文件",
required = true, schema = @Schema(type = "file", format = "binary")),
@Parameter(name = "type", description = "存储类型", example = "1")
})
@PostMapping(value = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public BaseRes upload(@RequestPart MultipartFile file,
@RequestParam Integer type) {
// 实现
}
推荐统一的分页响应格式:
java复制@Data
public class BaseRes<T> {
private Integer code;
private String msg;
private T data;
@Data
public static class PageData<T> {
private Long total;
private List<T> records;
}
public static <T> BaseRes<PageData<T>> page(Page<T> page) {
PageData<T> data = new PageData<>();
data.setTotal(page.getTotal());
data.setRecords(page.getRecords());
return success(data);
}
}
问题:@ApiOperationSupport的includeParameters在POST请求中无效
影响:无法在文档中只显示部分对象属性
临时方案:为不同接口创建专用的VO类
问题:设置多个@ApiResponse会导致响应状态显示为Tab页
影响:导出的文档会出现重复的响应状态描述
建议:为枚举类型添加详细描述:
java复制@Schema(description = "任务状态", enumAsRef = true)
public enum TaskStatus {
@Schema(description = "等待执行") PENDING,
@Schema(description = "执行中") RUNNING,
@Schema(description = "已完成") COMPLETED
}
经过这次升级,总结出以下几点经验:
对于新项目,建议直接采用Spring Boot 3.x + Knife4j 4.x的组合。对于老项目升级,需要特别注意注解变更和包名变化(javax → jakarta)。