SpringBoot 4.0.3配置Swagger——从翻车到顺手的完整踩坑记录
先说个真实场景:上个月有个老朋友找到我,说他们团队刚从SpringBoot 2.7升级到SpringBoot 4.0.3,结果原来好端端的Swagger界面全挂了,接口文档白茫茫一片。他照着网上那些教程反复排查了两天,什么springfox、springdoc、knife4j全都试了一遍,不是启动报错就是访问404,整个人快崩溃了。
我听完第一句话就问:“你用的springdoc-openapi是什么版本?”他愣了一下说:“不就是最新版吗?”问题就出在这里。SpringBoot 4.0.3是个大版本跃迁,底层框架从javax全面切换到jakarta,Springfox早就停止维护了,而旧版的springdoc-openapi也不兼容。如果还拿老经验去做新配置,翻车是必然的。
这篇文章我会从版本兼容性、依赖引入、核心配置、鉴权集成到常见问题排查,把SpringBoot 4.0.3接Swagger这件事掰开揉碎讲清楚。尤其是版本对应关系、过滤器放行规则、分组配置这些坑,全是实测踩出来的经验。适合正在做版本升级、或者其他项目要新接文档服务的同学参考,直接照着抄就行。
1. SpringBoot 4.0.3与Swagger的兼容性真相
1.1 版本选错,一切白费
先说结论:SpringBoot 4.0.3搭配Swagger,现在唯一靠谱的选择是springdoc-openapi的3.x版本,而且必须是某个具体的里程碑版本之后才行。我一开始用的还是2.8.x的老版本,启动直接抛NoSuchMethodError,后来翻官方仓库的release notes才确认,从SpringBoot 4.0开始,Jakarta EE版本已经从10升到了11,Spring Framework也升级到了7.0,整个Servlet API的语义都有变化,旧版springdoc-openapi压根无法感知这些新接口。
如果你还想用springfox,我劝你趁早死心。这个项目在2020年左右就停更了,官网首页写着“maintenance mode”,它的底层依赖还是springfox-swagger2那套老路径,连OpenAPI 3.0规范都没完整支持,更别说SpringBoot 4.x这种全新的自动装配机制。硬上的结果就是把整个项目拖垮,连正常启动都做不到。
我列一张在实际项目中验证过的对照表:
| SpringBoot版本 | 推荐Swagger方案 | 具体依赖 |
|---|---|---|
| 2.x | springfox 3.0.0 或 springdoc 1.x/2.x | springfox-swagger2 |
| 3.x | springdoc 2.x | springdoc-openapi-starter-webmvc-ui |
| 4.0.3 | springdoc 3.x | springdoc-openapi-starter-webmvc-ui |
| 4.0.3(WebFlux) | springdoc 3.x | springdoc-openapi-starter-webflux-ui |
有个关键点:SpringBoot 3.x和4.x用的Maven坐标前缀都是springdoc-openapi-starter-*,不是老式的springdoc-openapi-ui,后者在SpringBoot 3之后就不再生效了,我也踩过这个坑,换成starter系列后一切正常。
1.2 SpringBoot 4.0.3到底变了什么
很多人会问:SpringBoot 4.0.3不就是3.x的延续吗?配置Swagger怎么就有这么大差别?表面上只是版本号变了,其实改动相当多。
第一个大变化是完整的Jakarta迁移。SpringBoot 4.0.3的所有内置组件,包括spring-boot-starter-web、内嵌Tomcat,都基于Jakarta Servlet 6.1来构建,包名从javax.servlet.*变成了jakarta.servlet.*。如果第三方库还引用老包名,运行时会直接找不到类,最典型的表现就是启动日志里出现ClassNotFoundException: javax.servlet.Filter。Swagger作为涉及Web层的一系列Filter和Servlet,自然逃不过这个兼容性问题。
第二个大变化是自动配置机制改了。SpringBoot从3.x开始慢慢淘汰spring.factories,到4.0.3已经完全切换为AutoConfiguration.imports文件机制。Springfox这类老库还在用老方式注册自动配置,SpringBoot扫描不到,自然不会加载Swagger的配置类。
第三个变化是默认路径匹配策略。SpringBoot 4.0.3延续并且强化了PathPatternParser作为默认策略,替代了老式的AntPathMatcher。这会导致很多通配符写法失效,比如原来只配了/v2/api-docs/**而不配/v3/api-docs/**,就会出现明明配置了但访问不了的问题。
理解了这三个底层变化,你就能明白为什么网上那些SpringBoot 2.x的教程在4.0.3下完全失效——不是Swagger变了,而是整个生态变了,只能用适配新生态的方案。
2. 工程初始化与依赖引入
2.1 环境准备
既然是跟着实操走,我先说清楚我这边的基础环境,方便对照:
- JDK 17以上,推荐JDK 21,别用JDK 8
- Maven 3.9以上,Gradle 8.x也可以
- SpringBoot 4.0.3
- springdoc-openapi 3.x(具体子版本见后文)
JDK版本这块没什么好商量的,SpringBoot 4.0.3的最低要求就是JDK 17,如果你还在JDK 8上跑,连项目都启动不了。好消息是Swagger本身对JDK版本没有额外限制,所以环境上只要满足SpringBoot的基本要求就够了。
2.2 Maven依赖的精确写法
Maven项目的pom.xml里需要引入这两个核心依赖:
<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>3.0.0</version> </dependency>截止到这篇文章写作时,3.0.0是我实测下来最稳定的一个版本,它明确声明支持SpringBoot 4.0.x。如果你用Gradle,对应写法是:
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.0'有个细节需要留个心眼:不要顺手引入spring-boot-starter-validation之外的东西去“增强”Swagger功能。很多教程会说引入springdoc-openapi-ui或者swagger-annotations,但在SpringBoot 4.0.3下这些依赖往往和starter内部依赖冲突,反而导致/swagger-ui/index.html无法渲染。保持最小依赖集合,能跑起来再逐步加东西,这是我认为最省事的路径。
2.3 首次启动验证
依赖加好之后,直接启动项目,正常情况下控制台会打印一行特征日志:
o.s.springdoc.webmvc.ui.SwaggerWelcome: swagger-ui processed这说明springdoc已经成功注入了Swagger的UI组件。此时直接访问两个地址做验证:
- API文档JSON地址:
http://localhost:8080/v3/api-docs - Swagger UI页面地址:
http://localhost:8080/swagger-ui/index.html
如果两个地址都能正常返回内容,说明最基础的链路已经通了。如果访问/v3/api-docs返回404,优先检查你是不是还在依赖老版本的springfox,或者类路径里有没有历史遗留的Swagger依赖。我见过好多次,项目里同时存在两个Swagger相关依赖,包冲突导致路由全部失效,白白排查了一整天。
3. 核心配置与常用注解实操
3.1 配置文件里的关键参数
Swagger接好之后,不能直接扔着不管,生产环境的文档需要定制化。SpringBoot 4.0.3下,application.yml里最常用的配置段是这样的:
springdoc: api-docs: enabled: true path: /v3/api-docs swagger-ui: enabled: true path: /swagger-ui.html display-request-duration: true operations-sorter: method tags-sorter: alpha disable-swagger-default-url: true packages-to-scan: com.example.demo.controller paths-to-match: /api/**,/manage/** show-actuator: false几个参数单独解释一下:
packages-to-scan用来指定扫描哪个包下的Controller。如果你有多个模块,可以写多个包名,用逗号分隔。这个参数的本质是限定文档的展示范围,避免把项目里的监控接口、内部接口都暴露出来。
paths-to-match是做路径过滤的,只匹配以/api/或者/manage/开头的接口。这两个参数组合起来,基本可以做到“文档只看该看的”,既白又干净。
operations-sorter: method表示接口按HTTP方法排序,GET排在POST前面,这样UI上看起来更整齐。tags-sorter: alpha表示按控制器名称的字母顺序排列,多模块项目里这样排会更清晰。
还有一个比较冷门的参数display-request-duration,开启后UI会在每个接口旁边显示请求耗时,这对后续做接口性能观测挺有帮助,建议开发环境直接打开。
3.2 Java代码里的注解实战
依赖和配置文件都搞定后,代码层面只需要补充少量注解,Swagger就能把接口文档生成出来。最常用的三件套是:@Tag标在Controller类上,@Operation标在方法上,@Parameter标在方法参数上。
举一个实际项目中最常见的写法:
@Tag(name = "用户管理", description = "用户注册、登录、信息查询等接口") @RestController @RequestMapping("/api/user") public class UserController { @Operation(summary = "查询用户详情", description = "根据用户ID查询用户详细信息") @GetMapping("/{id}") public ResultVO<UserVO> getUser(@Parameter(description = "用户ID") @PathVariable Long id) { // 业务代码略 } }这里有个容易忽略的坑:如果项目里用了ResultVO<T>这种统一返回结构,springdoc会尝试把这个泛型结构里的每个字段都展示在文档里。如果ResultVO里有一个code、一个message、一个data还好,但如果你把Throwable异常对象也塞进去了,文档会变得极其冗长。解决办法有两个,要么在DTO里加@Schema(hidden = true)隐藏不需要的字段,要么用@ApiResponse手动指定返回结构,我一般推荐后者。
3.3 Swagger注解与OpenAPI注解的差异
这里要给从springfox迁移过来的读者做个提醒:springdoc体系使用的是io.swagger.core.v3的注解,比如@Tag、@Operation、@Schema,不是以前springfox用的io.swagger.annotations.*。如果你发现代码里写@Api、@ApiOperation,编译没问题但页面怎么也不显示,那说明你引入的还是旧注解,需要全部替换。
对照关系可以参考这张表:
| 旧写法(springfox) | 新写法(springdoc) |
|---|---|
| @Api | @Tag |
| @ApiOperation | @Operation |
| @ApiParam | @Parameter |
| @ApiModel | @Schema |
| @ApiModelProperty | @Schema(description = "xxx") |
| @ApiIgnore | @Hidden |
从数量上看,新注解的语义更清晰,参数也更简洁,但如果你写习惯了老注解,刚切换时会有些别扭。我的建议是直接改,不要做兼容层,因为springdoc本身就不认老注解,留着只会造成文档信息缺失。
3.4 分组配置的进阶玩法
如果你的项目是微服务架构,一个SpringBoot应用对外暴露很多模块接口,不想让所有接口混在一个文档里,可以用GroupedOpenApi做分组。这一步在我的日常开发中用处非常大,尤其是在联调阶段,前端只需要看自己负责的那部分接口。
新建一个配置类:
@Configuration public class OpenApiConfig { @Bean public GroupedOpenApi userApi() { return GroupedOpenApi.builder() .group("用户模块") .packagesToScan("com.example.demo.controller.user") .build(); } @Bean public GroupedOpenApi orderApi() { return GroupedOpenApi.builder() .group("订单模块") .packagesToScan("com.example.demo.controller.order") .pathsToMatch("/api/order/**") .build(); } }配置完之后,Swagger UI页面的右上角会多一个下拉框,可以直接切换分组。这对接口量超过50个的项目来说非常友好,不用在长长的列表里翻来翻去找接口。
这里有一个我踩过的坑:GroupedOpenApi之间如果扫描范围重叠,会出现同一个接口在多个分组里重复展示的情况。所以分组的扫描范围最好做到互斥,要么靠不同包名,要么靠不同路径前缀。实在没办法重叠的,用pathsToMatch把边界划清楚,比在代码里写exclude要省事得多。
4. 鉴权集成与生产级配置
4.1 给Swagger UI配置JWT全局鉴权
实际开发中,很多后端接口都要带Authorization头才能调用。如果接口文档里不带这个鉴权能力,前端拿去调试时每个请求都要手动拼Token,很烦人。
可以在配置类里声明一个全局的安全方案:
@Bean public OpenAPI customOpenAPI() { return new OpenAPI() .components(new Components().addSecuritySchemes("bearerAuth", new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT"))) .addSecurityItem(new SecurityRequirement().addList("bearerAuth")); }加上这段配置后,Swagger UI上会多出一个“Authorize”按钮,点击后输入Bearer <token>,之后调接口都会自动带上这个头,不用再手动填。
有个细节必须提醒:addSecurityItem是全局生效的,意味着所有接口都会带上鉴权标记。如果项目中有一部分接口是公开的(比如登录接口、注册接口),你需要在那些Controller方法上加@SecurityRequirements注解来取消这个全局鉴权,否则前端看到登录接口旁边有个锁的图标,容易产生疑惑。
4.2 Spring Security集成下的过滤器放行
大多数SpringBoot项目都会配上Spring Security,而一旦开启权限控制,Swagger页面默认会被拦截掉,显示403或者跳转到登录页。这个问题的本质是:Swagger的路径没被Spring Security标记为放行。
我的处理方式是在Security配置里把相关路径加进白名单:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers( "/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**" ).permitAll() .anyRequest().authenticated()) .csrf(csrf -> csrf.ignoringRequestMatchers("/swagger-ui/**", "/v3/api-docs/**")); return http.build(); }这个写法有两个关键点:一是所有Swagger相关路径必须加permitAll(),二是csrf也要对Swagger路径放行,否则即使权限放行了,Swagger UI页面可以打开,但从页面发起API调用时会被CSRF校验拦下来,前端看到的就是“请求失败”的红色提示。
如果你用了自定义的登录页,还需要额外留意Swagger UI里接口的调用流程。因为Swagger页面是在前端直接发Ajax请求的,不走原先的表单登录流程,所以你不要指望通过配置一个formLogin就让Swagger自动拿到登录态。要么用上面的Bearer Token方案,要么在测试环境临时放开鉴权,我一般推荐纯Token方案,对开发联调的影响面最小。
4.3 OAuth2.0配置的完整示例
部分项目走的是OAuth2.0认证体系,springdoc也支持对接。整体思路和JWT类似,只是SecurityScheme的类型换成OAUTH2,并额外声明授权流程和端点。
一个相对完整的写法:
new SecurityScheme() .type(SecurityScheme.Type.OAUTH2) .flows(new OAuthFlows() .authorizationCode(new OAuthFlow() .authorizationUrl("http://localhost:8080/oauth/authorize") .tokenUrl("http://localhost:8080/oauth/token") .scopes(new Scopes() .addString("read", "读取权限") .addString("write", "写入权限"))));这个方案写起来不难,但我要泼一盆冷水:OAuth2.0授权码模式在Swagger UI里的体验其实不算好。如果你的网关或者认证服务器做了跨域限制、Cookie策略调整,前端弹出的授权窗口经常会莫名验证失败。我实际项目里遇到的情况是,开发环境勉强能用,一上测试环境就各种跳转问题。后来大家还是改用JWT方案,出问题也更好定位。所以除非你们公司规定必须展示OAuth2授权流程,否则我还是建议优先用全局bearerAuth,省心太多。
4.4 生产环境的文档安全策略
最后一个部分聊一下发布问题。很多人习惯把Swagger一直开着,无论什么环境都暴露对外,这在安全层面是个隐患。接口文档会暴露项目结构、字段定义、接口调用方式,一旦被有心人扫到,等于把系统的攻击面直接画出来了。
我自己的习惯是开发环境打开、测试环境打开、生产环境关闭,并且把开关做成配置项:
springdoc: api-docs: enabled: ${SWAGGER_ENABLED:false} swagger-ui: enabled: ${SWAGGER_ENABLED:false}部署的时候,通过环境变量SWAGGER_ENABLED来控制。生产环境设成false后,/v3/api-docs和/swagger-ui.html都会直接返回404,不占用额外的接口暴露面。
如果你们生产环境确实需要在线文档给运维或者客服看,建议把Swagger放在内网环境或者通过网关做权限控制的路径上,不要直接暴露到公网。这是一个我从上线事故里学到的教训,原因不方便展开,只能说“接口文档裸奔”的下场很不好。
5. 常见问题与排查技巧实录
5.1 典型报错与解决方案
我把在SpringBoot 4.0.3下配置Swagger期间遇到的高频问题整理成了速查表,每一条都是亲手解决过的:
| 现象 | 根因 | 解决方案 |
|---|---|---|
| 启动报错NoClassDefFoundError | 引入旧版springdoc或springfox | 统一用springdoc 3.0.0及配套starter |
| 访问/v3/api-docs返回404 | 路径扫描配置错误或依赖冲突 | 检查是否有多套Swagger依赖,清理干净 |
| 页面打开但接口列表为空 | packages-to-scan没写对 | 确认Controller包路径,以及路径前缀是否匹配 |
| 接口返回字段全是空值 | 统一返回类型与泛型不兼容 | 用@Schema标注字段描述,避免复杂泛型 |
| UI页面显示403 | Spring Security拦截 | 放行/swagger-ui/和/v3/api-docs/ |
| 页面能看但调用接口报405 | PathPattern匹配策略变化 | 检查@Requestmapping路径和Swagger路径是否冲突 |
| 启动后内存明显升高 | 开启actuator且被扫描 | 设置show-actuator: false或paths-to-match只选业务路径 |
这里特别点名一下第一条“NoClassDefFoundError”。你如果看了堆栈信息,发现报错指向io.swagger.v3.oas.models.OpenAPI,那基本就是依赖版本冲突。我用mvn dependency:tree看到过项目里同时存在了3.0.0和2.2.0两个版本的swagger-models,原因是某个内部SDK传递依赖了一个老版本。解决办法是在pom.xml中用dependencyManagement锁定版本,或者排除内部SDK里不需要的Swagger依赖。
5.2 请求路径在Swagger中重复的坑
SpringBoot 4.0.3中,如果Controller类上写了类级别的@RequestMapping("/api/user"),方法上又写了@GetMapping("/{id}"),按常理来说Swagger展示的路径应该是/api/user/{id}。但如果你在application.yml里又额外配置了server.servlet.context-path: /demo,文档里会出现两种情况:
一种情况是Swagger UI展示的路径叫作/demo/api/user/{id},前端拿来直接调本地服务没问题;另一种情况是Swagger只展示应用的内部路径/api/user/{id},而前端实际跑的时候发现少了前缀,怎么调都是404。
这个问题的根源在于context-path是容器层面的路径,Swagger UI无法自动感知它的存在。我的建议是不要在本地开发环境配置context-path,统一由网关层做路由前缀,这样才能保证Swagger里看到的路径和真实接口路径一致。
如果项目已经用上了context-path,没法去掉,那还有一个临时方案:在OpenAPI配置类里手动拼接全局前缀,让文档路径和实际路径对齐。但这样做会引入双层路径的风险,一旦网关也做了前缀,会出现两个前缀叠写的情况,维护成本很高。
5.3 测试环境接口超时的排查
一个很常见但不太容易定位的问题是:Swagger UI页面打开正常,接口也能跑,但单个接口执行特别慢,动不动就超时。很多人以为是Swagger影响性能,其实多数情况下和Swagger本身没有关系。
我有一次排查到深夜才发现,是某个接口的执行链路里调用了对外部系统的同步HTTP调用,而那个外部系统在测试环境不稳定,每次调用要卡上好几秒。和Swagger完全无关,只是以前没有用Swagger直接调接口,所以没发现这个性能瓶颈。这个案例倒提醒了我一件事:Swagger不只是文档工具,它还是联调阶段最轻量的接口调试工具。你完全可以在UI上观察请求耗时,快速定位哪些接口存在性能隐患。配合上面提到的display-request-duration: true,这个效果会特别明显,能帮助你用很小的成本拿到接口级别的响应时间,对日常自测来说非常实用。
5.4 依赖冲突的终极排查思路
前面多次提到依赖冲突,这里给出一个标准的排查流程,希望帮你少走弯路:
- 用
mvn dependency:tree查看依赖树,重点搜索包含swagger、springfox、openapi关键字的依赖 - 在
pom.xml中对冲突依赖使用exclusion排除多余传递依赖 - 全局锁定Springdoc及Swagger相关组件版本,避免不同模块各自定义版本
- 清空本地Maven仓库中残留的旧包,重新编译
- 启动时观察日志中是否有“Swagger”相关的
WARN或ERROR输出
这个步骤其实适用于所有涉及Swagger的SpringBoot版本,不只是4.0.3。我在升级大版本时一定会做一轮这样的清理,因为历史遗留依赖是最容易埋雷的地方。
6. 最后再分享两个细节
第一件事,关于knife4j。国内很多开发喜欢用这个增强版的Swagger UI,界面更美观,也增加了离线文档导出等功能。但在SpringBoot 4.0.3下,它的适配版本和springdoc一样需要选择新的,就目前实际测试来看,兼容情况不如官方Swagger UI稳定。如果你不是特别依赖它增强的那些功能,我的建议是直接使用官方UI,减少一层适配风险。
第二件事,关于接口文档的团队协作。Swagger做到“能看、能调、能分组”只是基础,更值得投入的是让它作为团队内部接口契约的唯一事实源。我们在实际操作中会把Swagger页面的URL固化在团队文档里,所有新接手的开发直接打开页面查找接口定义,不再需要翻阅多人维护的Word文档。这样既避免了文档内容过期,也大幅降低了新人的上手成本。
回到开头那位朋友的问题。他后来按我给的版本组合替换了依赖,删掉了多余的旧包,并且把Spring Security的放行配置补上,前后不到半小时就把Swagger恢复了。他的原话是:“早知道版本对应关系这么重要,就不去瞎试了。”这大概就是我们今天聊这一大圈的核心意义:在技术选型和版本升级时,多花点时间确认生态兼容性,远比试错来得高效。