Spring Boot 3 发布后,我身边陆续有同事升级项目,几乎每个人都会在同一关卡住:原来的 Swagger 页面打不开了。报错还各不相同,有的直接启动失败,有的页面白屏,有的报Unable to infer base url。如果你也卡在这一步,这篇文章就是给你准备的。我会用 Spring Boot 3 + Knife4j 4.x 把接口文档完整跑起来,全程不碰 Swagger 2(也就是不带 springfox-swagger2 玩),从依赖到配置、从注解到排错,每一步都讲透。
1. Swagger 2 失效的根源:javax 到 jakarta 的大迁移
1.1 Spring Boot 3 的一次“伤筋动骨”的变更
先理清一个背景。Spring Boot 3.0 从底层做了重大调整,其中影响面最大的不是 JDK 版本升到 17,而是 Java EE 的命名空间从javax.*整体迁移到了jakarta.*。很多在我们看来雷打不动的老依赖,都在这次迁移中被拦在了门外。
听起来有点抽象,我打个比方。以前所有 Java 服务端程序都在用同一套“电话薄”,这里面存着 Servlet、JPA、Validation 等标准接口的调用方式,这套电话薄的目录前缀叫javax。Spring Boot 3 大笔一挥,把这套电话薄全部重印了,目录前缀都改成了jakarta。如果你的程序还在按老目录javax.servlet去找类,对不起,根本找不到。
Springfox 就是这套老电话薄的深度用户。它内部对javax.servlet、javax.validation这些类有强依赖,Spring Boot 3 里这些类已经换了新地址,Springfox 自然就成了“睁眼瞎”。
这里面还有一个更现实的原因:Springfox 的 GitHub 仓库已经很久没更新了。最后一个正式版本 3.0.0 停留在 2020 年,社区里大量的 issue 没人处理。你不是第一个踩坑的人,也不会是最后一个,只是这个问题永远等不到官方修复了。
1.2 老项目升级时最典型的三种报错
如果你是从 Spring Boot 2 直接往 3 升级的,大概率会遇到下面几种情况。
第一种,启动直接抛异常,常见的几句包括ClassNotFoundException: javax.servlet.Filter、NoClassDefFoundError: javax/validation/ConstraintValidator。这基本就是 Springfox 依赖了老命名空间下的类,类加载器找不到导致的。
第二种,应用能启动,但访问/swagger-ui.html或者/v2/api-docs直接 404。这是因为 Springfox 在 Spring Boot 3 的自动配置机制下没有成功注册,资源路径全都失效了。
第三种,页面能打开,但是页面里的接口列表加载不出来,控制台提示请求/v2/api-docs报错。这种情况说明你同时引入了多个 Swagger 相关依赖,版本互相冲突。
不管哪一种,解决思路都一样:从项目里彻底移除 springfox-swagger2 和 springfox-swagger-ui 这两个依赖,然后用兼容 Spring Boot 3 的方案重新搭建。
顺带提一句,我知道有人会问:那原来的 Swagger 注解还能用吗?比如@Api、@ApiOperation、@ApiModelProperty这一套。答案是:如果你继续用 Springfox 的注解,即使换了依赖也白搭,因为 Springfox 已经没人维护了。正确的做法是用 OpenAPI 3 规范下的新注解,也就是io.swagger.v3.oas.annotations.*这一套。后面我会专门写一节注解迁移对照,照着改就行。
2. 选型逻辑:为什么 Spring Boot 3 项目要用 Knife4j 而不是继续 Swagger 2
2.1 从 Springfox 的废墟上长出来的两个方案
Springfox 靠不住了,市面上还有别的选择吗?有,主流的是两个方向。
第一个方向是 springdoc-openapi。它是一个活跃维护的开源项目,从 Spring Boot 3 发布没多久就完成了适配,并且全面拥抱 OpenAPI 3 规范。它的做法是把接口文档的数据生成和 UI 展示分离,数据部分通过/v3/api-docs这个接口输出 JSON,展示部分默认提供一套 Swagger UI 页面。
第二个方向就是咱们今天的主角 Knife4j。很多老同学对 Knife4j 的印象还停留在“Swagger UI 的增强皮肤”,这个印象在 4.x 版本已经不完全准确了。Knife4j 4.x 的底层同样基于 springdoc-openapi,等于把接口数据的解析、聚合能力全部交给 springdoc,自己专注于把文档 UI 和周边体验做到极致。
所以我在这里先给一个重要的认知纠正:Knife4j 4.x 不是在 Swagger 2 上做增强,而是在 OpenAPI 3 这条新赛道上,基于 springdoc 提供了一套更符合国内开发者习惯的增强方案。
2.2 三张表格看清 springfox、springdoc、Knife4j 的差别
很多人在选型时犹豫不决,我把三者的核心差异整理成大表格,看得更清楚。
| 对比项 | springfox-swagger2 | springdoc-openapi | Knife4j 4.x |
|---|---|---|---|
| 规范支持 | OpenAPI 2(Swagger 2) | OpenAPI 3 | OpenAPI 3 |
| Spring Boot 3 兼容 | 不兼容 | 完全兼容 | 完全兼容 |
| 维护状态 | 已停更 | 活跃维护 | 活跃维护 |
| UI 地址 | /swagger-ui.html | /swagger-ui.html | /doc.html |
| API 文档地址 | /v2/api-docs | /v3/api-docs | /v3/api-docs |
| 接口调试能力 | 较弱 | 一般 | 强劲,支持全局参数、离线文档 |
| 文档分组 | 支持 | 支持 | 支持且体验更好 |
| 接口排序 | 不支持 | 有限支持 | 原生支持 |
| 自定义文档描述 | 有限 | 一般 | 支持 Markdown 补全 |
从表里能看出来,如果你只想要一个零依赖、纯官方的方案,springdoc-openapi 本身就够用了。但如果你需要给团队、给外部对接方提供一个更好用的文档站点,Knife4j 的体验确实是高一档的。
它好在哪里?我挑几个真实场景说。文档页面支持中文界面,搜索接口非常流畅;接口可以自定义排序和作者标记,前后端对需求的时候直接喊“张三负责的那个接口”就行;调试请求支持全局参数,把 token 配一次,所有接口的调试请求都自动带上。这些是在 springdoc 默认 UI 上需要写不少代码才能实现的功能。
2.3 标题说的“不带 swagger2 玩”,具体是什么意思
结合标题,我想把这句话说透。
以前在 Spring Boot 2 项目里整合 Knife4j,典型的做法是引入knife4j-spring-boot-starter,它内部依赖了 springfox-swagger2,然后你还要加一个@EnableSwagger2注解,这样才能把 Docket 注册进去。这是 Knife4j 2.x 时代的玩法,底层走的是 OpenAPI 2。
Spring Boot 3 项目不能再这么干了,必须改用 Knife4j 4.x 的 Jakarta 版本。新项目的依赖坐标是knife4j-openapi3-jakarta-spring-boot-starter,底层不再依赖 springfox,而是依赖 springdoc-openapi。与此同时,代码里不需要再写@EnableSwagger2,因为 springdoc 的自动配置会帮你处理一切,只需要显式声明一个OpenAPI的 Bean 来维护文档的基础信息。
这篇文章后面所有的步骤,都是在这个新的技术体系下展开的。我会在关键节点反复强调哪些旧习惯必须丢掉,避免你照搬老教程踩坑。
3. 整合步骤:依赖、配置类、yml 一次写到位
3.1 创建工程时的前置条件和依赖引入
先说你手头必须具备的条件:JDK 17+、Spring Boot 3.x、Maven 3.6+(Gradle 也行,不过我用 Maven 演示)。如果你项目还是 JDK 8,别急着升级整合,先把 JDK 升上来再说,Spring Boot 3 根本不支持 JDK 8。
新建项目时只要勾选Spring Web这一个依赖就够了,其他模块后续按需添加。然后打开pom.xml,加入 Knife4j 的依赖。
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> <version>4.5.0</version> </dependency>这里有一个很多人会忽略的细节:这个 starter 并不会自动帮你引入 springdoc-openapi 的全部能力,所以我倾向于在项目里显式把 springdoc 的依赖也加上,版本对齐 2.2.0 以上,这是兼容 Spring Boot 3 的版本线。
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.2.0</version> </dependency>如果你只引入 knife4j 的 starter,在实际运行时大概率会把 springdoc 的传递依赖也带进来,但版本不受你控制,一旦和你项目里的其他依赖冲突,排查起来很痛苦。所以我个人习惯是把两个依赖都写上,版本明确,出问题也好定位。
再补一句:如果你的项目里已经存在 springfox-swagger2、springfox-swagger-ui 或者其他任何以io.swagger:swagger-annotations为坐标的老依赖,请先全部移除。这一步不做,后面启动报错是必然的。
3.2 编写 OpenAPI 配置类:让文档有名字有作者
接下来创建一个配置类,核心是注入一个OpenAPI类型的 Bean。这个 Bean 负责声明文档的元信息,包括标题、描述、版本、联系人、License 等,最终会渲染在 doc.html 页面的顶部区域。
package com.example.demo.config; import io.swagger.v3.oas.models.ExternalDocumentation; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class Knife4jConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title("旺财记账系统接口文档") .description("本文档由 Knife4j 自动生成,包含旺财记账系统的全部 RESTful 接口。") .version("v1.0.0") .contact(new Contact() .name("技术开发组") .email("dev@example.com") .url("https://example.com")) .license(new License() .name("Apache 2.0") .url("https://www.apache.org/licenses/LICENSE-2.0"))) .externalDocs(new ExternalDocumentation() .description("项目在线文档中心") .url("https://example.com/docs")); } }注意一个点:OpenAPI这个类的完整包名是io.swagger.v3.oas.models.OpenAPI,不是旧版的springfox.documentation.spring.web.plugins.Docket。从 Docket 迁移到 OpenAPI 之后,最大的感受就是配置轻量了很多,不需要再去定义那些复杂的ApiInfo、PathSelectors、RequestHandlerSelectors了。
如果你的项目里有多个环境,比如开发、测试、预发,你可以在类上加@Profile({"dev", "test"}),让这个 OpenAPI Bean 只在特定环境生效。这样生产环境不会暴露任何接口文档信息。
3.3 application.yml 里的关键配置逐项拆解
配置文件是整个整合过程中最容易出幺蛾子的地方。我先给一份完整配置,然后逐项解释为什么这么写。
server: port: 8080 springdoc: api-docs: enabled: true path: /v3/api-docs swagger-ui: enabled: true path: /swagger-ui.html packages-to-scan: com.example.demo.controller paths-to-match: - /api/** - /admin/** knife4j: enable: true setting: language: zh_cn enable-footer: false enable-footer-custom: true footer-custom-content: Copyright © 2024 旺财记账团队逐条说明。
springdoc.api-docs.enabled决定是否生成接口 JSON,默认就是 true,建议显式写上,方便日后在某个环境一键关闭。
springdoc.api-docs.path是接口文档 JSON 的访问路径,Spring Boot 3 时代的默认值是/v3/api-docs,一般不用改。但如果你为了让接口风格统一,把项目的所有接口都加了一个统一前缀,比如/demo/v3/api-docs,那你需要手动同步改这里。常见做法是在server.servlet.context-path设置了项目上下文路径时,这个路径会自动带上上下文前缀,不需要额外处理。
springdoc.swagger-ui.path默认指向/swagger-ui.html。在 Knife4j 4.x 整合中,这个路径主要用于兼容旧的访问习惯,真正的主力 UI 地址是/doc.html,这个地址由 knife4j 自己接管。
springdoc.packages-to-scan是一个过滤选项。如果你的项目里有非常多无关的 Controller,比如内部监控接口、回调接口,它们不想出现在文档里,就通过这个配置限定只扫描指定包。默认情况下不配置也没问题,springdoc 会扫描整个 Spring 容器中的 Controller。
springdoc.paths-to-match按路径前缀过滤。比如我只想让/api/**和/admin/**开头的接口进文档,其余一律隐藏,就在这里配。这个配置在实际项目中非常常用,尤其是那些暴露了 actuator 端点的服务,一张文档页面干干净净很重要。
knife4j.enable是 Knife4j 增强功能的总开关。设置为 true 才能启用 doc.html 页面和全系列增强功能。如果你只是想用 springdoc 原生的/swagger-ui.html,把它设为 false 也无所谓,但我相信看完这篇文章的人不会这么做。
knife4j.setting.language把默认界面语言改成中文。虽然 Knife4j 默认也会根据浏览器语言切换,但直接写死成zh_cn可以避免团队里有人浏览器语言设置奇怪导致的乱码问题。
knife4j.setting.enable-footer-custom配合footer-custom-content可以把页面底部的版权信息换成自己的,顺带把默认的“Powered by Knife4j”去掉,公司内部文档看起来更正式。
3.4 启动验证:doc.html 正常打开的判断标准
配置完成后,直接启动项目。启动日志里如果出现knife4j相关的初始化信息,说明依赖加载没问题。然后浏览器访问:
http://localhost:8080/doc.html把 8080 换成你项目实际端口。页面打开后,你会看到一个左侧是接口分组、中间是接口列表、右侧是接口详情的文档界面。如果左侧能按月分组展示 Controller,接口点进去能看到参数说明和响应结构,说明整个链路已经通了。
在 Knife4j 4.x 里,springdoc 自带的/swagger-ui.html也仍然可以访问,只是样式比较朴素。我的建议是以后统一把/doc.html作为文档地址发给前端和测试同学。
4. 注解实战:从 @Api 到 @Operation 的迁移与增强
4.1 注解体系变了的底层原因
很多从 Swagger 2 时代过来的人,最头疼的就是注解全变了。其实这不是 Knife4j 故意折腾你,而是 OpenAPI 3 规范本身就把注解体系重构了。
OpenAPI 3 规范不再使用@Api、@ApiOperation、@ApiParam、@ApiModelProperty这一套,取而代之的是@Tag、@Operation、@Parameter、@Schema。这一改不是改名这么简单,底层的数据模型完全不同了。旧注解描述接口的方式是“给一个类打标记”,新注解的方式是“描述操作的每一个语义细节”。
使用新注解还有一个额外的好处:这些注解是 OpenAPI 规范的标准注解,属于io.swagger.v3.oas.annotations.*包,由 Swagger 官方维护。就算哪一天你不用 Knife4j、改用别的文档工具,这套注解依然有效,不会绑死在某一个框架上。
下面我按 Controller、方法参数、实体类三个层面,把最常用的写法过一遍。
4.2 Controller 层的官方注解写法
先看一个完整体面的 Controller 写法,我直接贴代码。
package com.example.demo.controller; import com.example.demo.common.Result; import com.example.demo.model.dto.UserCreateDTO; import com.example.demo.model.vo.UserVO; import com.github.xiaoymin.knife4j.core.annotations.ApiSupport; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.*; @Tag(name = "01. 用户管理", description = "用户注册、登录、资料修改等接口") @RestController @RequestMapping("/api/user") @ApiSupport(author = "张三", order = 1) public class UserController { @Operation(summary = "用户分页查询", description = "按条件分页查询用户列表,支持用户名模糊搜索") @GetMapping("/page") @ApiOperationSupport(order = 1) public Result<PageResult<UserVO>> page( @Parameter(name = "pageNum", description = "页码,从1开始", required = true, example = "1") @RequestParam Integer pageNum, @Parameter(name = "pageSize", description = "每页条数", required = true, example = "10") @RequestParam Integer pageSize) { // 业务代码省略 return Result.success(...); } @Operation(summary = "新增用户") @PostMapping @ApiOperationSupport(order = 2) public Result<UserVO> create(@RequestBody @Valid UserCreateDTO dto) { // 业务代码省略 return Result.success(...); } @Operation(summary = "根据ID获取用户详情") @GetMapping("/{id}") @ApiOperationSupport(order = 3) public Result<UserVO> getById( @PathVariable @Parameter(description = "用户ID", required = true, example = "1001") Long id) { // 业务代码省略 return Result.success(...); } }我逐行说几个容易理解偏差的点。
@Tag里的name属性在 Knife4j 页面中显示为分组名。我习惯在名称前面加数字前缀,比如01. 用户管理、02. 订单管理。为什么要加?因为 Knife4j 默认按 Controller 类名的字母序排列分组,加数字前缀能强制排序,让文档里的模块顺序符合业务逻辑,而不是字母序。
@Operation里的summary是接口列表里显示的一句话标题,description是点进去看到的详细描述。写 summary 的时候尽量一句话说清接口功能,比如“用户分页查询”就比“用户列表接口”强,因为列表这个词有歧义,是查询?是刷新?含糊不清。
@Parameter直接标在方法参数上,描述单个入参的含义。注意example属性非常有用,它会在调试区域自动填入示例值,前端同学测试时不用手动输入,效率提升很明显。
@ApiSupport和@ApiOperationSupport是 Knife4j 的增强注解,在类上加@ApiSupport标记作者和分组排序,在方法上加@ApiOperationSupport标记接口在分组内的排序。这是我整篇文章里最建议你用起来的两个注解,它直接提升了文档的可读性。
4.3 实体类上的 Schema 注解
实体类的注解主要作用于请求体和响应体的结构描述,Knife4j 会根据字段名和类型自动生成示例 JSON,但如果你想控制每个字段的说明文字和示例值,就需要用@Schema。
package com.example.demo.model.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Data @Schema(description = "用户信息返回对象") public class UserVO { @Schema(description = "用户ID", example = "1001") private Long id; @Schema(description = "用户名", example = "zhangsan") private String username; @Schema(description = "头像地址", example = "https://cdn.example.com/avatar/1001.png") private String avatar; @Schema(description = "账号状态", example = "1", allowableValues = {"0", "1"}) private Integer status; }一个实用技巧:@Schema的allowableValues属性如果你不写,Knife4j 根据字段类型是数字或者字符串,会自动渲染一个输入框;写了之后会渲染成下拉框,非常直观。
另外,如果你有某个字段不想出现在文档里,比如密码、密钥等敏感信息,可以在字段上加@Schema(hidden = true),或者在返回 VO 里干脆不定义这个字段。很多人把返回实体直接拿数据库实体用,导致文档里把用户密码哈希值都展示出来了,这在真实的项目里是要被安全审计打回票的。
4.4 从 Swagger 2 老注解迁移到 OpenAPI 3 的对照表
如果你手里是一个需要迁移的老项目,我这里整理了一份最常碰到的注解迁移对照表,直接照着改就行。
| 功能 | Swagger 2 注解 | OpenAPI 3 注解 |
|---|---|---|
| 分组/模块描述 | @Api(tags = "...") | @Tag(name = "...", description = "...") |
| 接口方法描述 | @ApiOperation(value = "...") | @Operation(summary = "...", description = "...") |
| 方法参数描述 | @ApiParam | @Parameter |
| 实体类别名 | @ApiModel | @Schema |
| 实体字段描述 | @ApiModelProperty | @Schema |
| 忽略接口/字段 | @ApiIgnore | @Hidden |
| 响应结构描述 | @ApiResponses | @ApiResponses(包名也变了,注意 import) |
还有一个细节:响应注解@ApiResponses在两组体系里的包名都差不多,但一个是io.swagger.annotations.*,另一个是io.swagger.v3.oas.annotations.responses.*,类名相同,包名不同。如果你 IDE 里 import 自动导入了错的包,编译能过,文档不生效,非常隐蔽,排查了好几回才总结出这个坑。
5. doc.html 访问后的三大异常完整排查链路
5.1 页面能打开但提示“文档请求异常”,接口列表加载不出来
这是我在各个技术群里看到最高频的问题。现象是/doc.html页面能打开,但顶部红色横幅提示“文档请求异常”,左边的接口列表一个都没有。
遇到这个现象,先别急着重启应用,按下面的步骤定位。
第一步,浏览器按 F12 打开开发者工具,切到 Network 面板,刷新页面,找到对/v3/api-docs发起的那个请求。看它的状态码。
如果状态码是 404,说明/v3/api-docs这个路径没有被正确路由。可能的原因有两个。第一个,项目设置了server.servlet.context-path,比如你的项目上下文路径是/demo,那么接口文档的实际地址应该变成/demo/v3/api-docs。你访问 doc.html 的时候,如果用了反向代理,代理没有把上下文路径保留,就会导致 404。第二个,你把springdoc.api-docs.path改成了别的值,和 knife4j 内部默认请求的路径对不上。
如果状态码是 401 或 403,那基本可以确定是被安全框架拦截了。这个问题我在 5.3 小节专门讲。
如果状态码是 200,但响应体不是预期的 JSON 文档结构,而是你们项目自定义的Result包装对象,说明你有一个全局的 ResponseBodyAdvice 在作怪。它拦截了所有 Controller 的返回,把/v3/api-docs的返回也包装了一层。解决思路是在增强代码里判断请求路径,遇到/v3/api-docs就跳过包装。
第二步,确认扫描范围。如果你在springdoc.packages-to-scan里配置了扫描包,但实际的 Controller 不在这个包下面,文档也会是空的。这个看起来很低级,但我和同事排查过好几次,最后发现是复制粘贴配置文件时把包名写错了。注意,这个配置一旦设置,就会完全覆盖默认的扫描路径,不能和默认行为叠加。
第三步,看启动日志。Knife4j 和 springdoc 在启动时有日志输出,比如springdoc的初始化信息会打印扫描到了哪些包、哪些 HandlerMethod。如果日志里出现了异常堆栈,多半是依赖冲突。最常见的冲突来源是swagger-annotations的旧版本被其他依赖传递引入了。解决办法是在 Maven 依赖树里搜索io.swagger:swagger-annotations,把老版本 exclusions 掉。
5.2 访问 /v3/api-docs 返回 404 或 200 但内容是空的
这个问题和上面的有关联,但单独列出来,因为还有一种特殊场景:页面能显示接口列表,但点击任意接口,右侧详情区一片空白。
先看直接访问接口文档 JSON 地址的情况,http://localhost:8080/v3/api-docs。如果返回 404,重点检查springdoc.api-docs.enabled没有被设置成 false。这种情况常见于你从网上复制了一份配置,里面有enabled: false,但你根本没意识到这是控制文档总开关的。
如果返回 200 但 JSON 内容里paths是空对象,说明 springdoc 没有扫描到任何接口。回头检查你的 Controller 类上有没有@RestController,或者@Controller+@ResponseBody的组合注解。不要笑,这个问题真的常见,因为很多老项目里 Controller 类上只写了@Controller和@RequestMapping,但方法上没有@ResponseBody。Springfox 时代能显示,是因为它的扫描逻辑在某些版本里对这类接口也能识别,但 springdoc 的识别在默认情况下更严格。
还有另一种情况:你的接口路径前缀五花八门,但你设置了springdoc.paths-to-match。这个配置的作用是“过滤”,不是“添加”。比如你写的是paths-to-match: /api/**,那所有不以/api开头的接口都会被隐藏。解决办法是检查你的接口路径是否符合这个前缀规则,或者干脆去掉这个配置。
5.3 被安全框架拦截:Spring Security 6 的放行写法
这是最容易被忽略但又最重要的一步。项目里一旦引入了 Spring Security、Shiro 这类权限框架,默认情况下所有请求都需要认证,doc.html 和数据接口自然也需要放行。
在 Spring Boot 3 项目中,Spring Security 已经升级到 6.x,配置写法相比旧版本有一个明显变化:antMatchers变成了requestMatchers,并且authorizeHttpRequests取代了旧的authorizeRequests。很多网上教程还是旧的写法,直接复制过来会报编译错误。
这里给一份可以直接用的 Spring Security 配置片段。
package com.example.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers( "/doc.html", "/webjars/**", "/v3/api-docs/**", "/swagger-ui/**", "/favicon.ico" ).permitAll() .anyRequest().authenticated() ) .csrf(csrf -> csrf.ignoringRequestMatchers("/doc.html", "/v3/api-docs/**")); return http.build(); } }有几个注意点。
/webjars/**必须放行,Knife4j 页面所需的静态资源都放在 webjars 路径下,不放行的话 doc.html 能打开但样式全丢。
/v3/api-docs/**后面的/**不能省。因为分组之后会有多个子路径,比如/v3/api-docs/user、/v3/api-docs/order,只放行/v3/api-docs的话分组文档依然被拦。
/doc.html需要放行,同时如果用了springdoc.swagger-ui.path配置,对应的路径也要放行。
还有一个容易踩的点:Spring Security 6 默认开启了 CSRF 防护,文档调试页面发 POST、DELETE 等非安全方法的请求时会被 CSRF 拦截。上面代码里我已经把/doc.html和/v3/api-docs/**的 CSRF 忽略掉了,但如果你在 doc.html 的调试功能里遇到了 403 的问题,别忘了回来检查这一行。
如果你的项目用的是自定义拦截器(HandlerInterceptor),不是安全框架,也要记得在addInterceptors配置里对这几类路径做excludePathPatterns放行。这种问题更隐蔽,因为拦截器只拦你看得见的业务接口,页面请求被拦了往往不会第一时间想到是自己写的拦截器干的。
6. 上线前必须处理的三个细节:关文档、分组、导出
6.1 生产环境一键关闭文档
开发环境里,接口文档是前后端协作的利器。但一旦部署到生产环境,公共网络可达的接口文档就是一个妥妥的信息泄露风险点。攻击者可以从文档页面快速摸清你的系统全貌,甚至直接对着接口调试工具发起批量请求。
最稳妥的做法是在生产环境配置文件中把文档彻底关掉。你不需要改代码,只需要在application-prod.yml里加上两行:
springdoc: api-docs: enabled: false knife4j: enable: false这样生产环境下,/doc.html返回 404,/v3/api-docs也返回 404,接口数据彻底不对外暴露。
如果你连 404 都不想暴露,可以在网关层或者 Nginx 层直接拦截/doc.html、/v3/api-docs、/swagger-ui/**这些路径,返回一个统一的无权限页面。考虑到安全审计的要求,我推荐在生产环境双重禁用:应用层关闭功能,网关层过滤路径。
还有一个容易被忽略的点:如果你们的接口文档是需要给外部客户看的,建议单独部署一个文档服务,只开放到测试环境,而不是把所有服务都暴露出去。
6.2 多分组配置:按模块拆分文档
当一个项目模块多了以后,所有 Controller 塞在一张文档页里会变得非常长,前后端同事翻起来都痛苦。Knife4j 支持多分组,可以按业务模块拆开。
配置方式很简单,在application.yml里使用springdoc.group-configs:
springdoc: group-configs: - group: user-center packages-to-scan: com.example.demo.controller.user - group: order-center packages-to-scan: com.example.demo.controller.order这个配置的含义是创建两个分组,user-center分组只扫描com.example.demo.controller.user包下的接口,order-center分组只扫描com.example.demo.controller.order包下的接口。
配置完成并重启后,再访问/doc.html页面,左上角就会多出一个下拉框,可以切换文档分组。同时,每个分组也会对应一个独立的接口文档 JSON 地址,格式是/v3/api-docs/{group},比如/v3/api-docs/user-center。
这里有一个非常实用的场景:你可以在网关层为每个分组配置独立的访问权限,比如给“内部管理模块”的分组加上 IP 白名单,而“开放平台模块”的分组走独立的鉴权逻辑。这样文档服务仍然是一个,但每个分组的暴露范围完全可控。
6.3 离线文档和自动生成客户端代码
Knife4j 页面右上角有一个下载离线文档的入口,支持导出 Markdown、Word、HTML 等格式。这在实际交付中很有用,尤其是那些不常访问文档页面的外部合作伙伴,你直接把离线文档发过去就行。
还有一个进阶用法值得提:因为 Knife4j 底层基于 OpenAPI 3,所以/v3/api-docs返回的 JSON 是标准的 OpenAPI 规范文件。你可以用这个 JSON 配合 OpenAPI Generator 工具,自动生成 Java、TypeScript、Go 等多种语言的客户端 SDK。
我自己在给第三方公司提供开放接口时,就是直接把这份 JSON 扔给对方的开发,让他们用自己习惯的语言生成客户端代码。这样既省了手写接口文档对接资料的时间,又保证了双方对接口定义的理解完全一致,字段名、参数类型、响应结构都不会有偏差。
如果你对接的团队用的是 YApi 这类接口管理平台,通常也支持通过 OpenAPI 格式导入,直接把 JSON 内容导入进去就能生成对应的接口列表,比手工录入高效得多。
最后再分享一个我个人的使用习惯:在项目进入稳定期后,我会把 doc.html 的地址直接配到内部知识库的固定页面里,并标注“请优先使用离线文档,紧急联调时再访问在线文档”。这样即使文档服务临时下线,也不影响团队成员查阅。接口文档这种基础设施,越稳定、越顺手,团队的整体开发效率就越高。