Spring Boot 3.x刚出来那阵子,几乎所有从2.x升上来的老团队都撞上过同一堵墙:以前项目里那个开箱即用的Swagger UI页面,升级后一访问就是空白页或者直接404。换了Springfox的新版本也不行,因为Springfox的维护基本停在了Spring Boot 2.x时代,面对Boot 3的Jakarta命名空间直接没了下文。我身边不少同事折腾一两天后,统一换成了springdoc-openapi这套方案,用下来确实稳。今天这篇就围绕在Spring Boot 3.x项目里引入springdoc-openapi这件事,把内置的Swagger UI、webmvc-api整套东西讲透,包括版本对照、依赖选择、配置写法以及实际排坑经验,给正准备升级或者新开3.x项目的朋友一个可以直接抄走的作业。
1. 为什么Spring Boot 3.x必须换掉旧Swagger方案(引入背景与选型)
1.1 javax到jakarta:一场命名空间的迁徙
Spring Boot 3.0做了一件影响面非常大的事:从javax.迁移到jakarta.。这不是换个包名这么简单,整个Servlet、JPA、Validation等基础API全部换了命名空间。以前写javax.servlet.http.HttpServletRequest的地方,3.x里全都得改写成jakarta.servlet.http.HttpServletRequest。这是一个上游规范的调整,Spring官方没有兼容选项,只能跟着走。
问题就出在这里:市面上几大Swagger方案都是基于javax命名空间构建的。Springfox的核心代码长期没有适配jakarta,导致Spring Boot 3.x启动后,Springfox的自动配置类在尝试处理接口文档时,要么类加载报错,要么直接静默失效。很多团队升级完Spring Boot 3.x,打开接口文档页面看到白屏,第一反应是配置写错了,实际上根因就是底层库不兼容。
这个迁移还牵扯到另一个隐蔽问题:Spring Boot 3.x要求JDK 17及以上,而JDK模块化对反射、代理等机制的约束更严格,老一代文档工具经常依赖一些非公开API的反射操作,在新JDK下也会触发InaccessibleObjectException。所以就算有人把包名硬替换成jakarta,反射层那一堆坑还是绕不过去。与其修补老方案,不如换一个从设计上就面向新生态的方案。
1.2 springfox为何在3.x集体失灵
Springfox维护者其实在Spring Boot 2.x时代就有点跟不上了。Springfox 3.0.0发布后,只支持到Spring Boot 2.6左右,后面Boot持续更新,Springfox并没有跟进兼容。到了Spring Boot 2.7,官方开始引入spring.mvc.pathmatch.matching-strategy=ant_path_matcher这种配置来兼容Springfox,其实已经是在打补丁。
等Boot 3.x出现,Springfox已经彻底断档。社区里很长一段时间流传着一个说法:Springfox项目实际上已经停止维护了,主页停留在老版本,Issues也没人回复。这不是说Springfox有多差,而是它在Spring Boot 3.x这个节点上已经尽完了历史使命。如果你用的项目还在Boot 2.x,继续用Springfox问题不大,但只要升到3.x,就不建议再跟它死磕。
后来官方用户手册里有一个专门的说明:Boot 3的Starter文档中不再包含Springfox相关内容,而是把Springdoc列为推荐的第三方文档方案。这个转折其实挺典型的,说明Spring官方自己也承认,接口文档这块第三方方案的选型已经变了。
1.3 springdoc-openapi的技术选型理由
springdoc-openapi从1.x时代就开始支持Spring Boot 2.x,而且它对OpenAPI 3规范的支持非常完整。真正让它脱颖而出的是2.x版本直接面向Spring Boot 3.x设计,artifactId重置,命名空间对齐,做到了“开箱即用,零配置出文档”。
我选它的理由还包括几个非常实际的点。
第一,它内置了Swagger UI。引入一个springdoc-openapi-starter-webmvc-ui依赖后,UI页面和OpenAPI JSON端点都会自动注册,不需要再单独引swagger-ui、swagger-core。这对很多只想让接口文档重新跑起来的人来说,是最省事的方式。
第二,它原生支持OpenAPI 3。这意味着可以充分利用@Tag、@Operation、@Parameter这些注解表达接口含义,而不是老旧的@Api、@ApiOperation,跟现在Spring生态的习惯也更一致。
第三,它支持分组文档。一个中大型后端项目接口可能上百个,全堆在一个文档页里根本没法看。springdoc可以按包、按注解、按请求路径做分组,每个组单独一个文档页面,这个在实际项目里几乎必用。
第四,它和Spring Security的整合很干净。要放行文档路径,只需要对/v3/api-docs/**和/swagger-ui/**做permitAll,这个配置非常透明,排查问题很容易。
2. 环境准备与依赖引入(固定版本,不用再猜)
2.1 版本对照:Boot 3.x与springdoc的版本对应关系
引入springdoc-openapi之前,先搞清楚版本对应关系,否则会出现各种奇怪的启动报错。springdoc-openapi的1.x系列对应Spring Boot 2.x,artifact是springdoc-openapi-ui;2.x系列对应Spring Boot 3.x,artifact是springdoc-openapi-starter-webmvc-ui。
我整理了一个简要的版本对照表供参考:
| Spring Boot版本 | springdoc-openapi版本 | artifact关键字 |
|---|---|---|
| 2.2及以下 | 1.2.x | springdoc-openapi-ui |
| 2.3 - 2.7 | 1.6.x / 1.7.x | springdoc-openapi-ui |
| 3.0 - 3.1 | 2.0.x - 2.2.x | springdoc-openapi-starter-webmvc-ui |
| 3.2及以上 | 2.3.x及以上 | springdoc-openapi-starter-webmvc-ui |
版本选择上我一般推荐直接选取2.x里较新的稳定release。因为springdoc迭代节奏比较快,新版本会修复Jakarta兼容细节和Spring Boot新版本带来的配置变化。截至现在的实践,2.6.x、2.7.x这类版本都相对成熟,大家可以在Maven Central上挑一个最近的正式版。
注意:如果你的项目用的是Spring Boot 3.2+,别再用1.x版本的springdoc或者Springfox,即使代码能跑起来,文档页也会出现各种异常情况,最终还是要换2.x。
2.2 Maven坐标与依赖树解读
Maven项目引入方式非常简单,在pom.xml里加一个依赖:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.6.0</version> </dependency>这一个依赖加完后,项目里会多出好几样东西:springdoc自身的核心模块、swagger-core、swagger-models、springdoc-ui以及swagger-ui的静态资源。也就是说,Swagger UI其实是被该starter间接带进来的,所以标题里说的“内置Swagger UI”指的就是这个效果,不需要再单独引org.webjars:swagger-ui之类的包。
如果项目是基于WebFlux的响应式工程,那需要用另一个starter:springdoc-openapi-starter-webflux-ui。两个starter的分工很清晰:webmvc-api这个系列是给Spring MVC传统Servlet栈用的。项目标题里特意点了webmvc-api,说明场景就是常规Spring Boot Web应用,用webmvc这个starter就对了。
讲一个我实际遇到的场景:曾有个同事为了图省事,直接复制了同事的webflux starter到webmvc项目里,结果接口文档一直空白,因为WebFlux starter注册的自动配置类和Spring MVC环境不匹配。虽然它不会强制报错,但自动配置会失效,文档模块静默不工作。排查了老半天,最后换成webmvc-ui就一切正常。所以这个artifact后缀真不是随便选的。
2.3 Gradle场景与模块取舍
Gradle项目同样简单:
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'如果不想引入整个UI模块,只想要OpenAPI JSON端点,也可以只引入springdoc-openapi-starter-webmvc-api。这个模块会生成/v3/api-docs这样的OpenAPI规范JSON,但不带Swagger UI页面。这种方案适合那些前端自己用OpenAPI JSON生成客户端代码、后端不需要UI的团队。不过说句实话,日常开发阶段我还是建议直接把UI带上,联调和排错效率会高很多,UI页面可以直接看接口的参数结构、响应模型,比肉眼盯JSON方便太多。
3. 核心配置实现(从零跑通Swagger UI)
3.1 最小的application.yml配置
引入依赖之后,其实什么都不配置就能启动文档功能。Spring Boot启动后,访问/swagger-ui/index.html就能看到UI页面,访问/v3/api-docs能拿到OpenAPI JSON。但实际项目中一般都会做一些定制,比如设置扫描包路径、自定义UI路径、调整分组。下面给一个最常见的起步配置:
springdoc: api-docs: path: /v3/api-docs enabled: true swagger-ui: path: /swagger-ui.html operations-sorter: method tags-sorter: alpha display-request-duration: true packages-to-scan: com.example.demo这段配置干了这么几件事:
api-docs.path:指定OpenAPI JSON端点的地址,默认就是/v3/api-docs,保持默认即可。swagger-ui.path:指定Swagger UI的访问路径,改成/swagger-ui.html符合老用户习惯。packages-to-scan:限定扫描哪个包下的Controller。如果不写,springdoc会自动扫描整个Spring上下文里所有Controller。项目小无所谓,项目大了建议还是显式配一下,能避免把一些内部系统的接口也暴露出来。operations-sorter:让UI页面里的接口按HTTP方法排序,GET在前、POST在后,看起来整齐很多。
这里涉及一个底层机制:springdoc在应用启动后,会在Spring MVC的RequestMappingHandlerMapping里读取所有已注册的接口元数据,然后把它们整理成OpenAPI文档模型。所以它能扫描到的接口,与Spring MVC注册的接口集合是一一对应的。有些接口如果用了自定义的HandlerMapping,或者不是标准的@Controller,springdoc可能扫不到,这点后面排查部分会展开。
3.2 自定义文档信息与分组
引入依赖后,默认的文档信息比较简陋,比如没有标题、没有版本、没有描述。可以在项目中写一个OpenAPI的Bean来定制:
@Configuration public class OpenApiConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title("订单服务API") .version("v1.0.0") .description("订单、支付、售后相关接口文档") .contact(new Contact() .name("后端研发组") .email("dev@example.com"))) .addServersItem(new Server() .url("https://api.example.com") .description("生产环境")); } }这里的info对应OpenAPI规范里的info节点,UI页面左上角会展示。addServersItem则是声明请求的base地址,如果不写,Swagger UI默认用当前页面的地址作为server,对本地联调来说没问题,但部署到测试环境带了context-path之后,往往会遇到调试地址不对的情况,所以一开始就把Server配好比较稳妥。
分组配置在项目稍微大一点之后几乎是刚需。比如订单模块和支付模块分开看,可以这么配:
@Bean public GroupedOpenApi orderGroup() { return GroupedOpenApi.builder() .group("订单模块") .packagesToScan("com.example.order") .pathsToMatch("/order/**") .build(); } @Bean public GroupedOpenApi payGroup() { return GroupedOpenApi.builder() .group("支付模块") .packagesToScan("com.example.pay") .pathsToMatch("/pay/**") .build(); }配置了多个GroupedOpenApi之后,Swagger UI页面右上角会出现一个分组下拉框,可以在不同模块之间切换。这个功能非常实用,尤其是接口数量到了大几十上百个,全在一个页面滚动查找完全没法用。
3.3 webmvc-api与Swagger UI的关系说明
标题里有个“webmvc-api”,很多初学者会疑惑这到底是什么东西。简单说,它指的是springdoc为Spring MVC应用生成的那份OpenAPI规范描述API,也就是/v3/api-docs这个JSON端点背后的实现模块。这份JSON里包含所有接口的请求方法、路径、参数、响应结构、数据模型等信息,Swagger UI的作用则是把这个JSON渲染成好看的交互页面。
两者是上下游关系,不是并列关系。真正提供给前端或测试工具使用的是这份JSON,Swagger UI只是它的可视化外壳。除了Swagger UI,任何符合OpenAPI规范的客户端工具都能消费这份JSON,比如Postman可以直接导入,后端可以根据它生成TypeScript客户端,测试平台可以拿它做自动化校验。理解了这层关系后,遇到“UI能用但接口文档JSON访问不到”之类的问题,就能大致判断方向是/v3/api-docs这条链路的配置出问题了。
3.4 与Spring Security的权限放行配置
实际项目里几乎都集成了Spring Security,这种情况下文档路径默认是受保护的,没登录就访问会跳转登录页。需要把文档相关路径放行:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() .anyRequest().authenticated() ); return http.build(); } }这里要注意,/v3/api-docs/**必须带上后面的/**,因为实际请求会有/v3/api-docs、/v3/api-docs/swagger-config等子路径。只管放行/v3/api-docs一个地址的话,UI页面能打开,但页面里拉取JSON的请求可能还会被拦截,控制台会看到401。
我以前在一个内网系统里踩过这个坑:UI页面上方一直转圈,打开浏览器开发者工具发现/v3/api-docs返回401,就是这个/**没写全。补上之后一切正常。也就是说,权限放行不只是为了打开UI首页,更要确保UI后续发起的那些异步请求都能通。
4. 实操过程:完整跑通一次
4.1 新建项目与依赖引入
我这里用一个最简单的Spring Boot 3.x项目来演示整套流程。创建工程这一步可以直接用Spring Initializr,选择Java 17,Spring Boot版本选3.3.x或者3.4.x。如果用的是Idea里的Spring Initializr,注意在Dependencies里不用选任何额外组件,后面我们在pom里自己加springdoc依赖。
pom.xml核心内容:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.4</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.6.0</version> </dependency> </dependencies>spring-boot-starter-web是必须的,因为springdoc的webmvc模块依赖Spring MVC环境。同时它会带来Tomcat、Jackson等组件,Spring Boot 3.x自带的是Jackson 2.x,和springdoc配合没有问题。至于热部署、测试包等按需加,本次不做多余引入。
注意:不要用
spring-boot-starter-webflux,那是响应式Web环境。如果项目中同时存在webmvc和webflux依赖,Spring Boot会优先自动配置WebFlux,springdoc的webmvc-ui模块可能不会被激活,这又是一个很隐蔽的启动问题。
4.2 一个简单的REST接口示例
写一个标准的Controller,确保接口能被扫描并展示在文档中:
@RestController @RequestMapping("/api/order") @Tag(name = "订单接口", description = "订单相关操作") public class OrderController { @Operation(summary = "查询订单详情", description = "根据订单ID查询订单信息") @GetMapping("/{id}") public OrderDetail getOrder(@Parameter(description = "订单ID") @PathVariable Long id) { return new OrderDetail(id, "示例订单", "PAID"); } @Operation(summary = "创建订单") @PostMapping @ResponseStatus(HttpStatus.CREATED) public OrderDetail createOrder(@RequestBody @Valid OrderCreateRequest request) { return new OrderDetail(1L, request.getProductName(), "CREATED"); } }再补一个数据模型类:
public record OrderDetail(Long id, String productName, String status) {}这里用@Tag、@Operation、@Parameter这些注解表达接口信息。它们是io.swagger.v3.oas.annotations包下的注解,对应OpenAPI 3规范。有些老项目还在用@Api、@ApiOperation,那是Swagger 2时代的注解,在springdoc下虽然老注解不会报错,但效果已经不同步了,建议迁移到OpenAPI 3注解。
4.3 启动验证与页面观察
启动应用后,按顺序验证三个端点:
# 1. UI主页面 http://localhost:8080/swagger-ui/index.html # 2. OpenAPI规范JSON http://localhost:8080/v3/api-docs # 3. 带分组后的配置端点 http://localhost:8080/v3/api-docs/swagger-config打开Swagger UI页面后,能看到订单接口这两个操作,点击接口展开,能看到请求参数、响应结构、数据模型定义。如果配置了OpenAPI的Bean,页面左上角会显示标题“订单服务API”。如果页面空白,先看控制台有没有异常,或者直接访问/v3/api-docs测试JSON是否返回,再双击刷新一下UI。
一个快速判断技巧:直接访问/v3/api-docs,如果浏览器能返回一段结构化JSON,说明服务端链路OK,问题大概率出在UI静态资源或路径放行上面;如果JSON都访问不到,就说明springdoc自动配置没有生效或应用上下文不兼容,优先检查starter类型和版本。
5. 常见问题与排查技巧实录
5.1 404与Whitelabel Error Page
新手最常遇到的就是启动后访问/swagger-ui.html或/swagger-ui/index.html返回404白屏。这类问题多半不是springdoc没生效,而是配置的路径和实际注册路径不匹配。
默认情况下,Swagger UI的首页地址是/swagger-ui/index.html,配置了springdoc.swagger-ui.path=/swagger-ui.html后,/swagger-ui.html会做一个转发,但真正的资源入口还是在/swagger-ui/**下。有些团队因此以为只配置path就够了,结果直接访问带index.html时反而404。建议配置完路径后,明确在文档或启动日志里记录UI地址,别让团队里不同人用不同地址访问,这个细节看着小,维护时很影响效率。
还有一类404是项目设置了server.servlet.context-path。比如context-path是/myapp,那么UI地址就变成http://localhost:8080/myapp/swagger-ui/index.html。如果前端项目里配了代理,注意将context-path一并带上。这个情况下,OpenAPI JSON的地址也会跟着变成/myapp/v3/api-docs。
5.2 接口扫描不到
如果你的Controller已经写好了,但Swagger UI里看不到对应接口,先检查两件事。
第一,包扫描范围。默认情况下,springdoc会扫描Spring Boot主类所在包及其子包。如果你的Controller在主包之外,建议显式配置springdoc.packages-to-scan,或者用@Bean GroupedOpenApi来指定更精确的扫描范围。第二,接口是不是用了非标准的路由注册方式,比如WebMvcConfigurer里手动注册的HandlerMapping,或者某些框架内部注册的接口。这些接口在RequestMappingHandlerMapping中能找到的,多半没问题;但如果接口是通过编程式路由建的,springdoc不一定能识别,需要额外处理。
还有一个容易被忽略的点:接口类上如果没有@RestController或@Controller注解,只是简单地用@RequestMapping标注在一个普通类上,springdoc不会扫描到。解决办法是统一Controller注解规范,这本身也是代码规范问题,不只是文档问题。
5.3 与Spring Security集成时404
集成了Spring Security后出现404,通常不是真的资源不存在,而是请求被安全链路拦截后重定向了。因为UI页面会发起/v3/api-docs、/v3/api-docs/swagger-config等请求,任何一个子路径没放行都会导致UI加载失败。
我在实际项目里还遇到过一种情况:security放行了/swagger-ui/**但没放行/v3/api-docs/**,结果UI页面能打开,但转圈加载不出来。这里建议直接执行请求,确认一下/v3/api-docs有没有被重定向到登录页,从而判断是否属于Security导致的请求拦截。如果存在网关或统一鉴权平台,也要确认网关层有没有把这两个路径透传过去,否则即使服务端放行了,网关拦在前面也一样404。
5.4 版本不匹配导致的启动异常
如果启动日志里出现ClassNotFoundException: javax.servlet.*或者NoClassDefFoundError: jakarta.servlet.*,十有八九是springdoc版本和Boot版本不匹配。比如在Boot 3.x里用1.x版本springdoc,或者反过来在Boot 2.x里用2.x版本springdoc。
解决办法很简单:把springdoc升到2.x,并确认artifact用的是springdoc-openapi-starter-webmvc-ui,不是老的springdoc-openapi-ui。如果项目是从Boot 2.x升级上来的,还需要把代码里Springfox时代的注解换掉,否则即使文档能启动,接口描述信息还是老式注解的注释表达,无法与OpenAPI 3模型兼容。
6. 避坑汇总与后续扩展方向
6.1 项目状态与文档质量
接口文档这东西,配置好了不代表万事大吉。我对文档质量的判断标准很简单:团队里任何一个人打开Swagger UI,不需要翻源码,就能知道每个接口的参数含义、必传字段和响应结构。要达到这个效果,@Operation描述必须写清楚,DTO字段上该加的@Schema注解也得加上,不然UI页面里只能看到一堆名为field1的裸字段。
springdoc有一个很方便的能力:它会自动从Jackson和Bean Validation注解中提取字段约束信息。比如DTO里的@NotBlank、@Min、@Max,很多会体现在OpenAPI文档的schema节点中。所以写请求DTO的时候,尽量把校验注解写完整,不仅代码层面受益,文档层面也会自动变清晰。我就见过有团队完全不写校验注解,文档里所有字段都是“可选”,联调时全靠口口相传,效率非常低。
6.2 我给新人的配置清单
结合近期的项目实践,我给出一份可以直接参考的“标准配置单”,适用于大多数基于Spring Boot 3.x的Web API项目:
springdoc: api-docs: path: /v3/api-docs enabled: true swagger-ui: path: /swagger-ui.html display-request-duration: true operations-sorter: method tags-sorter: alpha packages-to-scan: com.example paths-to-match: /api/**这个配置有几个用意。paths-to-match可以只暴露/api/**下的接口,把健康检查、内部处理接口排除在文档外。display-request-duration让UI页面显示每次请求耗时,调试时比较有用。packages-to-scan则保证扫描范围可控。不同团队可以根据自己的接口前缀调整,不一定非得是/api,但建议至少设一个统一前缀,这样文档和网关路由的对应关系也更清晰。
如果项目同时需要多个环境展示,可以在OpenAPIBean里声明不同环境的Server。多环境配置用Spring Profile也很方便。这类配置一旦沉淀成团队规范,后续新项目直接复制粘贴,能省掉很多重复沟通成本。
6.3 结合代码生成与自动化测试的补充思路
springdoc生成的这套OpenAPI JSON,除了人工在浏览器里查看,还有一个很大的价值是给自动化工具消费。如果你的团队在用Postman,可以把/v3/api-docs导入Postman,一键生成全部接口集合,省去手工逐个录入的麻烦。
更进一步,OpenAPI JSON还能配合OpenAPI Generator之类的工具产出TypeScript客户端代码,前端开发完全不用关心后端接口的路径和参数拼装。不过这里要提醒一点:代码生成的前提是后端文档质量足够高,字段命名、类型映射、枚举定义都得清晰。如果文档里一堆字段没有说明,生成的客户端代码也只是把垃圾代码换个语言而已。所以这个方向虽然好,但它不是“配完了再补文档”就能享用的,必须把写文档当作写接口的一部分。
6.4 我实际使用中的一点体会
从Spring Boot 2.x升级到3.x的那个迁移期,我把springfox从项目里彻底移除后,重新用springdoc搭文档,整个过程其实只花了一个下午。最费时间的不是配置本身,而是把老代码里的@Api、@ApiModelProperty这些注解逐个替换成@Tag、@Operation、@Schema。替换完以后,文档展示和代码内联提示都舒服了很多,之后在几个新项目里再也没折腾过接口文档相关的问题。
springdoc这套方案,最大的优点在于它紧跟Spring生态的节奏,不需要你去理解一堆底层适配细节,只要版本对得上、包路径对了,它就能安安静静地把活干完。如果你正在或即将启动一个基于Spring Boot 3.x的后端项目,我建议直接把springdoc-openapi当成默认依赖加进去,它会成为你开发联调过程中一个非常省心的存在。