做后端的,谁没被前端同事追着问过"这个接口返回字段啥意思""这个参数必填吗""有没有示例值"。最早期我们项目API文档是写在Confluence上的,Word风格那种,接口一改文档忘改,半个月后文档和代码完全对不上,前端照着文档调接口全是400,最后还是得抓着后端口对口确认。
后来上线了Knife4j,文档直接从代码注解生成,接口变了注解也得改,至少改完文档跟着变。我们Spring Boot 3.4.3的项目里用的是Knife4j 4.4.0,基于OpenAPI3规范,整套配置和踩坑过程整理一下分享出来。
一、为什么必须要API文档
不是矫情,是真能省事。
前后端联调。前端拿到一份能在线点开、能直接发请求试的文档,效率比看Markdown高一个数量级。我们前端Vue那边的同学,基本是开着Knife4j页面对着调,请求参数点"调试"按钮直接发,看到响应示例再决定字段怎么处理。比口头交流或者翻聊天记录靠谱太多。
新人理解接口。新人入职第一周不可能把所有Controller读完,但有了文档可以按模块快速浏览,知道系统有哪些接口、各自负责什么。我们项目11个模块的Controller,新人靠Knife4j两天就能摸清接口地图。
接口自测。后端自己开发完一个接口,在Knife4j里点调试、传参数、看响应,比开Postman新建集合省事。尤其配合Bearer认证配置好之后,登录拿token填一次,后续所有接口都自动带,调试链路特别顺。
文档这件事,要么代码即文档,要么文档即负债。Word文档这条路走过的人都懂,最后一定变成"文档没人维护、内容全是过期信息"的死局。
二、技术选型:为什么是Knife4j 4.4,不是Swagger2也不是SpringDoc
Spring Boot 3这一刀切下来,老项目的Swagger2(SpringFox)直接不能用了。原因很简单——Spring Boot 3把javax.*全换成了jakarta.*,SpringFox依赖的还是javax.servlet,依赖一拉进来就报ClassNotFound。SpringFox官方也基本停止维护了,社区里只能找到一些fork版本,谁敢往生产上引。
剩下两条路:SpringDoc和Knife4j。
SpringDoc是OpenAPI3的原生实现,维护活跃,Spring Boot 3支持得很好。但它的UI就是OpenAPI标准的Swagger UI,能用,但功能比较朴素——没有接口搜索、没有全局参数配置记忆、没有离线文档导出、调试体验也一般。
Knife4j 4.x底层就是基于springdoc-openapi做的,注解和API规范完全沿用OpenAPI3,但前端UI重写了一套,明显更顺手:左侧支持接口搜索、可以记忆上次填的参数、文档导出支持Markdown和HTML、还有请求参数缓存。对国内开发者更友好的是UI默认中文。
我们选的是knife4j-openapi3-jakarta-spring-boot-starter版本4.4.0,starter直接带好了SpringDoc依赖,不需要单独再引springdoc-openapi-webmvc-core,避免版本冲突。jakarta这个后缀很关键,对应Spring Boot 3的jakarta包名,老的非jakarta版本拉进来还是会被javax卡死。
三、Knife4jConfig配置类:三个核心Bean
整套配置就一个Knife4jConfig类,里面三个核心部分。
第一是OpenAPIBean,定义文档的全局元信息:title、version、description、contact(姓名、邮箱、url)。这些信息会显示在文档首页。Contact建议写真邮箱,方便外部对接的人联系到维护者。
第二是GroupedOpenApi,按模块分组。这块下面专门讲,11个模块就是11个GroupedOpenApi Bean。
第三是Bearer认证相关,通过OpenAPI的components.securitySchemes和security字段配置。securitySchemes定义一个名为Bearer的方案,type是http,scheme是bearer,bearerFormat是JWT;security字段把这个方案设成全局默认,这样所有接口在文档里都会显示一个锁的图标,表示需要认证。
配置knife4j.enable=true开启增强功能,这个属性在application.yml里。增强功能开了之后,UI才有接口搜索、文档导出这些能力。
四、注解使用:四个层级
OpenAPI3的注解体系比Swagger2清爽,io.swagger.v3.oas.annotations包下,按层级分四类。
Controller级的@Tag。每个Controller类上加@Tag(name = "用户管理", description = "用户的增删改查"),name会作为左侧菜单的分组名,description是补充说明。name不能重复,重复的话UI上会合并到一起,很难看。我们一开始有个同事图省事都写"管理",结果11个模块全挤在一个分组下,后来强制约定name要带模块前缀。
接口级的@Operation。方法上加@Operation(summary = "分页查询用户", description = "支持按用户名、手机号、状态过滤")。summary是接口列表里那行短标题,要简洁;description是详情页的长说明。这里有个坑下面专门讲——description里换行用\n在UI里不生效,得用<br>。
DTO级的@Schema。实体类字段上加@Schema(description = "用户ID", example = "1001", requiredMode = REQUIRED)。description是字段说明,example是示例值,requiredMode控制是否必填。example这个属性一定要填,前端看文档最希望看到的就是真实示例值,光有描述不够直观。
参数级的@Parameter。方法参数上加@Parameter(description = "页码", required = true, example = "1"),主要给路径参数和查询参数补充说明。@RequestParam这种简单参数其实用@Parameter就够了,但复杂对象入参用@Schema标注DTO更合适。
还有一个@Hidden注解,加在方法上可以让接口从文档里隐藏。内部接口、调试接口、不希望对外暴露的,用这个屏蔽掉,比单独写配置过滤方便。
五、Bearer认证集成:让Knife4j带上Sa-Token的JWT
我们项目用Sa-Token做认证,登录后返回JWT token,前端把token放在Authorization头里,格式是Bearer {token}。Knife4j默认是不会带这个头的,调试接口会直接401。要让Knife4j自动带上token,核心是配置SecurityScheme。
在OpenAPIBean里,components.addSecuritySchemes("Bearer", new SecurityScheme().type(Type.HTTP).scheme("bearer").bearerFormat("JWT").in(In.HEADER).name("Authorization")),定义一个名为Bearer的方案,类型是HTTP Bearer,放在请求头的Authorization字段。
然后openApi.addSecurityItem(new SecurityRequirement().addList("Bearer")),把Bearer方案加到全局SecurityRequirement里。这一行是关键——加上之后所有接口默认都关联这个认证方案,UI上每个接口旁边都会有个锁的图标。
实际使用的时候,用户点页面右上角的"文档管理"→"Authorize",弹出框里把登录拿到的JWT token粘进去(不要带Bearer前缀,Knife4j会自动加),点确认。之后调试任何接口,Knife4j都会自动在请求头里带上Authorization: Bearer {token}。
这里有个小细节要注意:token要填纯token值,不要手贱带Bearer前缀。我刚开始配置完测试,token前面加了Bearer,结果请求头变成Authorization: Bearer Bearer xxxxx,后端解析失败401,排查了好几分钟才发现是重复了。
六、生产环境安全:必须关闭文档
API文档这东西,开发环境是利器,生产环境是漏洞。
为什么生产必须关?两个原因。第一是接口暴露——所有接口的路径、参数、返回结构对任何能访问到文档URL的人都是透明的,相当于把系统结构白送人。第二是参数泄露——example里填的示例值如果是真实业务数据(比如真实手机号、订单号),生产环境就泄露了。
我们用Profile控制:application-dev.yml和application-test.yml里knife4j.enable=true,application-prod.yml里knife4j.enable=false。Spring Boot 3.4的Profile激活用spring.profiles.active=prod。
光配置knife4j.enable=false还不够,这个属性控制的是Knife4j的UI增强,但OpenAPI的JSON接口/v3/api-docs默认还在,接口元数据照样能拿到。所以还得在Spring Security或者Sa-Token的拦截器里把文档路径都拦截掉,prod环境直接404或者403。我们项目里Sa-Token配置了一个SaInterceptor,prod环境把/doc.html、/swagger-ui/**、/v3/api-docs/**、/webjars/**、/favicon.ico这些路径全部拦截。
还有一个常见疏漏:网关层。如果项目前面有Nginx反代,prod环境的Nginx配置里也要把这些路径deny掉,否则应用层关了网关层没关,等于没关。
七、11个模块的分组展示
我们项目11个模块:系统管理(用户/角色/菜单/部门)、AI供应商、AI模型、AI对话、AI知识库、代码生成器、文件管理、定时任务、系统监控、操作日志、字典管理。
每个模块一个GroupedOpenApi,关键配置是packagesToScan。比如系统管理模块:
GroupedOpenApi.builder().group("01-系统管理").packagesToScan("com.xxx.system.controller").build()
group名前面加数字前缀是为了控制UI上的显示顺序,Knife4j默认按字母序排,01开头的系统管理会排在最上面。如果不加前缀,"AI模块"会排在"系统管理"前面,看着别扭。
packagesToScan要写Controller所在的全限定包路径,不能写错。我们一开始有人把controller写成Controller(大写C),结果分组里有这个模块但接口数为0,排查半天才发现是包路径大小写问题——Java包名是大小写敏感的。
跨模块的Controller,比如有个公共的文件上传Controller被多个模块引用,可以单独建一个"00-公共接口"分组,packagesToScan指向公共包。这样既不污染业务模块的分组,又能在文档里展示出来。
OpenAPI主文档(group为default的那个)我们不展示,直接把springdoc.default-flat-param-object=true配上,让所有接口都通过具体分组访问,避免default分组和模块分组重复显示接口。
八、踩坑记录
jakarta包名导致的依赖冲突。这个最坑。一开始引的是knife4j-openapi3-spring-boot-starter(没有jakarta后缀),4.4.0版本,结果启动直接报ClassNotFoundException: javax.servlet.http.HttpServletRequest。Spring Boot 3的servlet包是jakarta,老starter还是javax。解决方案是换成knife4j-openapi3-jakarta-spring-boot-starter,只差一个词,但能差死人。Maven依赖这种东西,少个词、多 个词,表现就是天差地别。
@Operation的description换行不生效。description里写"第一行\n第二行",UI渲染出来还是一行,\n直接显示成字面字符。换成System.lineSeparator()也不行。最后发现Knife4j的description渲染走的是HTML,要用<br>标签才能换行。"第一行<br>第二行"就正常了。这个反直觉的地方不知道坑了多少人。
SSE接口在文档里显示异常。AI对话的流式接口,返回类型是text/event-stream,一开始@Operation没特别配置,文档里把响应类型识别成application/json,调试的时候一直转圈不出结果——因为Knife4j的调试器在等JSON响应结束,SSE是流式的一直不发完。后来给SSE接口加produces = MediaType.TEXT_EVENT_STREAM_VALUE,再配合@ApiResponse显式声明响应类型,文档显示就正常了。调试SSE接口还是得用浏览器原生EventSource或者curl,Knife4j的调试器对长连接支持不好,这个不能强求。
分组packagesToScan写错。前面提过,包名大小写敏感。还有一个坑是逗号分隔的多个包路径,packagesToScan接受的是可变参数,不是逗号分隔字符串。一开始有人写成packagesToScan("com.xxx.a.controller,com.xxx.b.controller")——一个字符串当成一个包名,结果两个包都没扫到。正确写法是packagesToScan("com.xxx.a.controller", "com.xxx.b.controller"),两个字符串参数。
Knife4j的basic认证弹窗。Knife4j有个knife4j.basic.enable配置,开启后访问/doc.html会弹basic认证框。一开始我以为这个开了之后Bearer token就不用单独配了,其实是两码事——basic认证是访问文档站点本身的,Bearer是接口业务层的认证。两个都要配,不能省。
生产环境漏关OpenAPI端点。前面提到的,光配knife4j.enable=false不够,/v3/api-docs还能访问。我们一开始上线就漏了这个,幸亏安全扫描发现了,赶紧把Sa-Token拦截规则补上。这个坑很多人都踩过,原因是knife4j.enable这个名字有迷惑性,让人以为关了它就万事大吉。
写在最后
Knife4j这套东西不算复杂,但真要在Spring Boot 3项目里用顺,从依赖选型到配置到注解到生产关闭,每一步都有坑。我们这套配置在11个模块的项目里跑了挺久,文档体验比早期Word时代好太多,前后端联调的扯皮次数明显减少。
几个心得:一是Spring Boot 3的jakarta变更影响面比想象大,凡是依赖servlet API的库都要核对jakarta版本;二是生产环境关闭文档要彻底,UI、JSON端点、网关层一个都不能漏;三是注解要写就写完整,summary、description、example、requiredMode都填上,半残的文档比没文档更坑人——前端看到没example的字段会按自己的理解填,调试出问题又得回来问。
OpenAPI3规范加上Knife4j的UI,在Spring Boot 3项目里是目前最顺手的API文档方案。如果你也在做类似的集成,希望这篇能帮你少走点弯路。