最近用 SpringBoot3 升级一个老项目时踩了个非常典型的坑:/v3/api-docs这个 OpenAPI 的 JSON 接口访问得干干净净、一点问题没有,可/swagger-ui.html就是打不开,直接 404。换成/swagger-ui/index.html依旧白页,折腾了整整两个晚上才把根因彻底理顺。
这个现象在 Springdoc 2.x + SpringBoot3 这个组合里其实非常常见。网上能搜到的帖子要么只给一个答案,要么就是不区分具体场景,抄过来也没用。这篇文章我直接把自己踩过的坑、每一步排查的思路、以及最终验证可用的完整配置全部摆出来。文章适合正好卡在“api-docs 能访问但 UI 打不开”的朋友,也适合刚准备给 SpringBoot3 项目接 Swagger 文档、想少走弯路的人。
1. 先复现现象,搞清楚“不能访问”到底是哪种不能访问
1.1 我遇到的现场
我的环境是SpringBoot 3.2.x + JDK 17 + springdoc-openapi-starter-webmvc-ui 2.3.0,内嵌 Tomcat 部署。项目启动后所有业务接口正常,但访问文档页时出现了非常分裂的状态:
curl -i http://localhost:8080/v3/api-docs # 200,返回一大段 OpenAPI JSON curl -i http://localhost:8080/swagger-ui.html # 404,Whitelabel Error Page curl -i http://localhost:8080/swagger-ui/index.html # 404,同样打不开浏览器直接访问/swagger-ui.html,出现的是 Spring Boot 默认的错误页,Tomcat 日志里也没有任何明显的异常堆栈。这个现象最迷惑人的地方在于:既然/v3/api-docs能正常输出,说明 Springdoc 的核心功能已经生效了,为什么 UI 偏偏不行?
这里需要先纠正一个误区:无法访问 UI 不等于 Springdoc 没生效,它只是说明 Swagger UI 这一层没有正常工作。如果你也遇到同样情况,先不要急着怀疑版本,更不要盲目重装依赖,而是要按下面第 2 节的分析,搞清楚 api-docs 和 swagger-ui 在 Spring MVC 里完全就是两条不同的路由。
1.2 常见的三种“HTML 无法访问”表现
根据我后来在网上翻帖子和自己复现的经验,Springdoc 场景下的“HTML 无法访问”基本有三类:
| 表现 | 典型状态码 | 大概率原因 |
|---|---|---|
| 页面直接 404 | 404 | 依赖缺 UI 包、静态资源映射被改、路径不对 |
| 页面返回 403 或被重定向到登录页 | 403 / 302 | Spring Security 拦截了 swagger-ui 路径 |
| 页面返回 200 但白屏或显示一堆 HTML 源码 | 200 | JS/CSS 加载失败、响应 Content-Type 不对、代理配置问题 |
这几种情况的原因和解决办法完全不同。你只有先确认自己属于哪一种,再去搜解决方案,才不会像我一样绕远路。
2. 为什么 api-docs 能访问而 html 不行:两类路由的差异
2.1/v3/api-docs走的是 Controller 映射
Springdoc 在启动时会向 Spring 容器注册一个 OpenAPI 相关的@RestController,把/v3/api-docs映射到具体的 Controller 方法上。换句话说,这个地址本质上是一个普通的 MVC 接口,和你自己写的@RestController没有任何区别。
只要 springdoc 的核心 API 库被正确加载,/v3/api-docs就会作为 DispatcherServlet 管理的一个普通端点存在。它不依赖任何静态资源,也不依赖 webjars,所以哪怕 Swagger UI 的资源包完全没有,这个接口依然能正常返回 JSON。这也是为什么很多人看到 api-docs 正常,就不去怀疑依赖问题的原因。
我当时排查时也犯了这个错误,一直在 api-docs 的逻辑里打转,完全没意识到 UI 是另一套资源。
2.2/swagger-ui.html走的是静态资源映射
Swagger UI 本质上是一个纯前端项目,由一堆 HTML、CSS、JavaScript 文件组成。这些文件被打包在 webjars 依赖里,路径通常位于classpath:/META-INF/resources/webjars/swagger-ui/。
Springdoc 的自动配置会在启动时注册一个ResourceHandler,把/swagger-ui/**这个 URL 路径映射到上面的 webjars 目录。访问/swagger-ui.html时,Springdoc 还会做一次转发,把它重定向到/swagger-ui/index.html。
所以在 Spring MVC 内部,这两条链路是这样的:
| 路径 | 类型 | 依赖 | 可能被影响的因素 |
|---|---|---|---|
/v3/api-docs | Controller 方法 | springdoc 核心库 | DispatcherServlet 是否可用 |
/swagger-ui.html | 静态资源映射 | springdoc UI 库 + webjars 资源 | 静态资源配置、Security、依赖版本、代理 |
这种差异直接解释了一个关键结论:api-docs 能访问只说明核心库没问题,它无法证明 UI 所依赖的资源包、静态资源映射和访问控制没有问题。
2.3 实际排查时要把两条链路分开看
我后来总结出一个很实用的排查原则:把“API 数据路径”和“UI 资源路径”当成两个独立的问题去看。
如果/v3/api-docs挂了,说明 OpenAPI 数据生成有问题,通常和依赖版本、配置冲突有关。如果发现/v3/api-docs正常但/swagger-ui.html挂了,那就把注意力放到静态资源、安全拦截、反向代理这三件事上。这样排查范围一下子缩小了很多,也避免在错误的层次上浪费时间。
3. 逐个排查:大多逃不过这 5 类原因
3.1 依赖引错或只引了一半
这是最常见的原因,没有之一。很多人用 SpringBoot3 时,只引入了下面这个依赖:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc</artifactId> <version>2.3.0</version> </dependency>注意:springdoc-openapi-starter-webmvc这个包只包含 OpenAPI 核心能力和自动配置,它不包含 Swagger UI 的前端静态资源。所以它能保证/v3/api-docs正常,但/swagger-ui.html就会 404。
如果你需要 UI 页面,必须引入带ui后缀的完整包:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.3.0</version> </dependency>另外还要注意几点:
- 如果是 WebFlux 项目,要引
springdoc-openapi-starter-webflux-ui,而不是 webmvc 版本。 starter-webmvc-ui这个依赖已经包含了 api 核心能力,不需要额外再引starter-webmvc,否则可能出现重复的 OpenAPI 资源,在某些版本下反而会引发奇怪问题。- 检查一下 pom 里是否残留了 Springfox(springfox-swagger2)相关依赖,Springdoc 和 Springfox 同时存在时不一定会直接报错,但争抢路径的情况非常坑。
我在自己项目里最终定位到的原因就是这个:之前只引了starter-webmvc,压根没引入 UI 包。属于那种“看起来简单,但一旦漏掉就怎么调都调不出来”的问题。
3.2 Spring Security 把页面拦截了
如果你项目里引入了spring-boot-starter-security,那依赖没问题也可能打不开 UI。Spring Security 在 SpringBoot3 中默认会拦截所有请求,未认证访问/swagger-ui.html时,通常会被重定向到登录页,或者因为在配置里没放开这些路径而返回 403/404。
SpringBoot3 使用的是 Spring Security 6.x,配置风格已经从旧的WebSecurityConfigurerAdapter换成SecurityFilterChain。如果你在网上搜到的是过时的写法,直接复制会报错。一个标准的放行配置像下面这样:
@Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers( "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/v3/api-docs.yaml", "/webjars/**" ).permitAll() .anyRequest().authenticated() ); return http.build(); } }这里几个关键点:
- 用
requestMatchers而不是旧版的antMatchers,后者在 Spring Security 6 里已经移除。 - 放行路径不仅要包含
/swagger-ui.html,还要放行/swagger-ui/**,因为index.html加载之后还会请求大量 JS/CSS 文件,这些都属于/swagger-ui/**路径。 /v3/api-docs/**也要放行。否则可能出现 UI 页面能打开,但页面一直转圈,控制台报“Failed to load API definition”的尴尬情况。- 如果你配了 Spring Security 的链路,建议用
curl -i看一眼是否有 302 跳转,如果有,基本就是被安全拦截了。
3.3 自定义静态资源配置把 swagger-ui 的路由冲掉
这种情况相对隐蔽。比如有些项目需要在/static/目录下放静态资源,会自定义WebMvcConfigurer:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/"); } }这段代码本身没太大问题,但如果你注册的 handler 路径过于宽泛,比如/**,在某些情况下会影响 Springdoc 自动注册的/swagger-ui/**映射。
更常见的是在application.yml里做了这种配置:
spring: mvc: static-path-pattern: /static/**这行配置把 Spring MVC 默认的静态资源匹配模式从原来的/**改成了/static/**。而 Swagger UI 的资源位于 webjars 下,对应的默认映射是/webjars/**。一旦你改了全局静态路径,webjars 的默认映射可能就不生效了,导致/swagger-ui/**的资源全部 404。
解决办法有两个方向:
- 尽量不要修改
spring.mvc.static-path-pattern,除非你非常清楚自己在做什么。 - 如果项目确实需要自定义静态路径,可以在
WebMvcConfigurer里显式补上 webjars 的映射:
registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/");即使全局静态路径被改了,只要补上这一条,/swagger-ui/**所需要的 webjars 资源就能重新被路由到。
3.4 版本不兼容:SpringBoot3 必须用 Springdoc 2.x
这是个坑中坑。SpringBoot2 时代,大家用的 Springdoc 是org.springdoc:springdoc-openapi-ui:1.6.x,这个版本的内部实现基于javax.*命名空间。SpringBoot3 全面迁移到了jakarta.*命名空间,如果你在 SpringBoot3 项目里直接用了 1.x 的 springdoc,很可能启动阶段就报ClassNotFoundException或NoClassDefFoundError,甚至静默地导致 UI 不生效。
正确做法是引入 2.x 系列的依赖:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.6.0</version> </dependency>版本号可以用目前最新的 2.x 稳定版。这里还要提醒一下:检查 pom 时不要只看显示名称,要用mvn dependency:tree | grep springdoc看一下实际解析出来的版本。曾经见过父级 BOM 或者传递依赖把 springdoc 版本覆盖回 1.x 的情况,这种问题在配置里根本看不出来。
3.5 反向代理/Nginx 导致页面资源加载不出来
生产环境普遍会用 Nginx 做反向代理。这种情况下有个典型现象:后端直接访问/swagger-ui.html是好的,但通过 Nginx 域名访问就是白屏,按 F12 看到一堆 JS/CSS 请求 404。
原因多半是 Nginx 的 location 只转发到了根路径,后续浏览器请求/swagger-ui/swagger-ui.css、/swagger-ui/index.css这些静态资源时,没有被转发到后端,或者被另一个静态服务拦截了。
一个基础的 Nginx 配置示例:
server { listen 80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /swagger-ui/ { proxy_pass http://127.0.0.1:8080/swagger-ui/; } location /v3/api-docs { proxy_pass http://127.0.0.1:8080/v3/api-docs; } }如果后端项目设置了server.servlet.context-path,比如/demo,那 Nginx 的转发也要对应加上前缀,否则/swagger-ui.html访问的链路是通不上的。
还有一类不太容易想到的问题:Nginx 没有配置 MIME 类型表,导致 HTML、CSS、JS 文件返回时Content-Type是application/octet-stream。浏览器拿到这种响应后不会按页面渲染,而是直接当成文件下载,或者显示成纯文本源码。这种情况在html 文件无法预览的搜索结果里非常常见。解决办法是在 Nginx 的http块里加上:
include /etc/nginx/mime.types;加了之后重启 Nginx,再访问 swagger-ui 页面,如果之前的症状是“显示一堆 HTML 标签源码”,这个操作大概率能解决。
3.6 页面返回 200 但一直转圈或加载不出接口列表
还有一种情况:/swagger-ui.html返回 200,页面也出来了,但中间位置一直转圈,控制台提示 “Failed to load API definition”。
这是 Swagger UI 页面启动后,会自己去请求一个 OpenAPI 数据源,默认就是/v3/api-docs。如果这个地址被安全框架拦截、被代理规则挡住、或者因为路径前缀不对导致 404,UI 就会一直卡在加载中。
排查方向:
- 单独访问一下
/v3/api-docs,确认返回的是 200 和 JSON 内容。 - 如果前面配了 Spring Security,检查
/v3/api-docs/**是否放行。 - 如果项目有 context-path 或网关前缀,检查浏览器地址栏里请求的 api-docs 路径是否正确。
还有一种可能是 Springdoc 配置了自定义urls或config-url,指向了一个根本不存在的地址。比如设置springdoc.swagger-ui.config-url: /foo/bar.json,而/foo/bar.json又没有对应的接口,就会导致 UI 加载失败。
4. 直接给一套能用的完整配置
如果你不想逐条排查,可以直接参照这一套配置。这是我目前用的组合,在 SpringBoot3 项目里实测可跑。
4.1 Maven 依赖
<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.3.0</version> </dependency>如果项目里已经有 spring-boot-starter-security,保留即可,不需要为了 swagger 去掉安全框架。
4.2 application.yml 配置
springdoc: api-docs: enabled: true path: /v3/api-docs swagger-ui: enabled: true path: /swagger-ui.html operations-sorter: method tags-sorter: alpha display-request-duration: true这里几个配置项的含义:
springdoc.api-docs.path就是 OpenAPI JSON 的访问路径,默认是/v3/api-docs,可以不写。springdoc.swagger-ui.path是 UI 页面入口地址,默认是/swagger-ui.html。注意这里的 path 虽然看似是 HTML 文件,但实际还是由 Springdoc 转发到/swagger-ui/index.html。operations-sorter和tags-sorter只是 UI 展示层面的排序规则,不写不影响功能。- 如果不想让 Swagger UI 自动加载默认的 petstore 示例,可以加上:
springdoc: swagger-ui: disable-swagger-default-url: true否则打开页面后,右上角可能有一个指向 petstore 的默认下拉项,虽然不影响使用,但第一次看会觉得有点突兀。
4.3 Spring Security 放行配置
如果引入了 Spring Security,参考下面的配置:
@Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers( "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/v3/api-docs.yaml", "/webjars/**" ).permitAll() .anyRequest().authenticated() ); return http.build(); } }如果项目还配置了 OAuth2 资源服务器或自定义过滤器,放行路径要确保在过滤器链中不会被前置过滤器拦截。
4.4 Nginx 反向代理配置
生产环境用 Nginx 时,参考如下:
server { listen 80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /swagger-ui/ { proxy_pass http://127.0.0.1:8080/swagger-ui/; } location /v3/api-docs { proxy_pass http://127.0.0.1:8080/v3/api-docs; } }同时确认http块里引入了mime.types,避免静态资源 Content-Type 错误。
5. 实战排查步骤:三步定位问题
5.1 用 curl 分层判断,别急着开浏览器
遇到这种问题,我强烈建议先用命令行工具做分层判断,而不是直接打开浏览器看。浏览器有缓存、有 JS 执行环境,反而干扰定位。
第一步,确认核心 API 数据是否正常:
curl -I http://localhost:8080/v3/api-docs如果返回 200,说明 Springdoc 核心链路没问题。
第二步,确认 UI 入口地址的状态:
curl -I http://localhost:8080/swagger-ui.html curl -I http://localhost:8080/swagger-ui/index.html观察返回的状态码和 Location 头。
第三步,确认静态资源是否可访问:
curl -I http://localhost:8080/swagger-ui/swagger-ui.css这个请求如果 404,问题基本可以锁定在依赖或静态资源映射上。
5.2 用依赖树检查 Jar 包
mvn dependency:tree | grep springdoc输出里应该能看到springdoc-openapi-starter-webmvc-ui以及相关的swagger-uiwebjar。如果看到的是springdoc-openapi-ui而且是 1.x 版本,说明依赖引错了;如果只有springdoc-openapi-starter-webmvc而没有带ui的包,说明 UI 资源压根没进类路径。
这个命令也能顺带排查是否同时存在 Springfox 等冲突依赖。
5.3 临时关闭 Security 验证
如果你怀疑是 Spring Security 搞的鬼,最快速的方式是临时排除安全自动配置,验证一下:
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})改完以后重新启动,再访问/swagger-ui.html。如果此时能正常打开,说明问题就出在安全放行配置上,把配置按第 4.3 节调整即可。如果排除后依然打不开,那问题基本不在安全层,继续查依赖和静态资源映射。
这个操作只能用在本地验证,不要在生产环境直接关闭安全配置。
5.4 检查自动配置是否生效
SpringBoot3 中可以通过启动时加--debug参数来打印自动配置报告:
mvn spring-boot:run -Ddebug然后在输出里搜索Swagger或springdoc,看相关的SwaggerUiWebMvcConfigurer是否处于matched状态。如果看到的是negative match,说明某些条件没有满足,顺着提示去看缺少了什么。
这个命令输出的信息量非常大,排查过 Spring Boot 自动配置问题的人应该不陌生,但新手看了可能会觉得太乱。其实只需要关注包含springdoc、swagger、webjars的这些片段即可。
6. 常见问题速查表
最后整理一个速查表,方便大家对照自己的现象直接定位,不用从头再读一遍:
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
/v3/api-docs返回 200,/swagger-ui.html返回 404 | 只引了starter-webmvc,没有引 UI 包 | 换成springdoc-openapi-starter-webmvc-ui |
/swagger-ui.html返回 403 或跳转登录页 | Spring Security 拦截 | 放行/swagger-ui/**、/v3/api-docs/**、/webjars/** |
| 页面 200 但白屏,F12 里 JS/CSS 404 | 静态资源映射被覆盖或代理未转发/swagger-ui/** | 检查static-path-pattern;补 Nginx location |
| 页面显示 HTML 源码 | Nginx 未加载mime.types | 在http块里include mime.types; |
启动报javax.servlet相关错误 | Springdoc 1.x 与 SpringBoot3 不兼容 | 升级到 Springdoc 2.x |
| 页面能打开但一直转圈,控制台报 API definition 加载失败 | /v3/api-docs被安全拦截或路径不对 | 放行 api-docs,检查 context-path 和代理前缀 |
/swagger-ui.html跳转/swagger-ui/index.html后还是 404 | webjars 资源缺失或浏览器缓存 | 强刷缓存,检查依赖树,直接访问/swagger-ui/index.html |
7. 总结一下我的排查顺序
这个问题真正难的地方不是解决方案本身,而是它背后的故障现象会诱导你在错误的方向上反复试探。我现在遇到类似问题,会严格按这个顺序来:
第一步,用curl看/v3/api-docs和/swagger-ui.html的状态码,先确认到底是哪一层挂了。第二步,跑mvn dependency:tree | grep springdoc,确认 UI jar 是否真的在依赖里。第三步,如果涉及安全框架,临时 exclude 掉 Security 自动配置做验证。第四步,如果以上都正常,再考虑代理、路径前缀、Content-Type 这些外部因素。
另外还有一个小建议:在项目里显式配置springdoc.api-docs.enabled和springdoc.swagger-ui.enabled,不要完全依赖默认值。这样后续排查时,至少能确定配置开关没有被隐式改掉。如果你接口数量多,还可以配合springdoc.group-configs做接口分组,Swagger UI 左上角会多一个分组下拉框,文档管理体验会好很多。
希望这篇踩坑记录能帮你少熬一个晚上。