springdoc-openapi-starter-webmvc-ui,而不是当前的
springdoc-openapi-starter-webflux-ui(版本均为 2.8.8)。
选错会导致 Swagger UI 能打开但拉取不到任何接口数据。
01两个依赖的本质区别
webmvc-ui 与 webflux-ui 并不是「UI 皮肤」的差异,
而是对应 Spring 生态里两套完全不同的 Web 技术栈:
| 维度 | webmvc-ui |
webflux-ui |
|---|---|---|
| 目标技术栈 | Servlet 栈(阻塞式) | Reactive 栈(响应式 / 非阻塞) |
| 底层 Web 框架 | spring-boot-starter-web + Tomcat/Jetty |
spring-boot-starter-webflux + Netty |
| 请求 / 响应类型 | jakarta.servlet.*(HttpServletRequest、ModelAndView) |
org.springframework.web.reactive.*(ServerWebExchange、Mono/Flux) |
| 端点发现方式 | 扫描 RequestMappingHandlerMapping 上的 @RestController |
扫描 RouterFunction Bean 及响应式 @RestController |
| 文档端点注册 | OpenApiResource 以 MVC Controller 方式暴露 /v3/api-docs |
以 RouterFunction 方式暴露 /v3/api-docs |
02核心区别:阻塞 Servlet vs 非阻塞 Netty
这是理解「为什么必须选对」的关键。两个依赖背后是两种并发处理模型,性能特征与编程模型截然不同。
线程池模型
- 每个请求占用一个独立线程(Tomcat 默认约 200 线程)
- 线程在等待 IO(数据库、远程调用)时会阻塞挂起,无法处理其他请求
- 并发量受线程池大小限制,线程过多导致内存与上下文切换开销大
- 编程模型:命令式、顺序执行,简单直观、易调试,生态成熟
事件循环模型
- 基于 Reactor(
Mono/Flux)+ Netty 事件循环 - 少量线程(约等于 CPU 核数)即可承载大量并发连接
- 等待 IO 时线程不阻塞,通过回调 / 事件驱动继续处理
- 编程模型:响应式链式,高吞吐低占用,但复杂、难调试,生态相对少
RequestMappingHandlerMapping 拿 HandlerMethod,WebFlux 通过
RouterFunction 与响应式 Controller 拿路由。
springdoc 内部用不同代码路径去扫描,选错栈就无法发现接口。
03自动装配条件(字节码证据)
两个核心配置类都带有 @ConditionalOnWebApplication 门控,且类型互斥:
SpringDocWebMvcConfiguration → @ConditionalOnWebApplication(type = SERVLET)
SpringDocWebFluxConfiguration → @ConditionalOnWebApplication(type = REACTIVE)
也就是说:应用是哪种栈,就只会有对应的那个配置类被激活,另一个直接不生效。
04本项目属于哪一类
证据链非常明确,本项目是 Servlet / MVC 栈:
passport-starter/pom.xml依赖spring-boot-starter-web(而非 webflux)- 控制器 import 的是
jakarta.servlet.http.HttpServletRequest与org.springframework.web.servlet.ModelAndView - 全项目无
spring-boot-starter-webflux、无Mono/Flux/RouterFunction
05用错的后果
因为 SpringDocWebFluxConfiguration 带 type=REACTIVE 条件,
在 Servlet 应用里根本不会被激活:
/v3/api-docs不会被注册 → Swagger UI 页面能打开(webjar 静态资源在), 但拉不到任何接口数据(空文档或 404)- 自定义的
OpenAPIBean 仍会注册(它只是元数据,不依赖 webflux), 但没有地方消费它来渲染文档
06依赖选择(正确配置)
把依赖从 webflux-ui 改为 webmvc-ui,版本保持 2.8.8:
<!-- passport-starter/pom.xml 与 bom/pom.xml -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
07API 配置说明
7.1 官方 springdoc 属性(写在宿主 application.properties)
| 属性 | 默认值 | 说明 |
|---|---|---|
springdoc.api-docs.enabled |
true | 是否暴露 /v3/api-docs |
springdoc.api-docs.path |
/v3/api-docs | api-docs 端点路径 |
springdoc.swagger-ui.enabled |
true | 是否开启 Swagger UI |
springdoc.swagger-ui.path |
/swagger-ui.html | 访问入口(会 302 到 /swagger-ui/index.html) |
springdoc.packages-to-scan |
主类所在包 | 只扫描指定包(隔离时建议显式声明) |
springdoc.paths-to-match |
/** | 匹配的路径模式 |
# 是否暴露 /v3/api-docs
springdoc.api-docs.enabled=true
springdoc.api-docs.path=/v3/api-docs
# 是否开启 Swagger UI
springdoc.swagger-ui.enabled=true
springdoc.swagger-ui.path=/swagger-ui.html
# 只扫描指定包,避免扫到宿主业务包
springdoc.packages-to-scan=com.sparrow.passport
springdoc.paths-to-match=/**
7.2 自定义元信息属性(starter 提供的命名空间)
starter 通过 @ConfigurationProperties(prefix = "sparrow.passport.openapi")
暴露元信息,宿主项目可按需覆盖:
| 属性 | 默认值 | 说明 |
|---|---|---|
sparrow.passport.openapi.enabled |
true | 是否注册 starter 提供的 OpenAPI 元信息 Bean |
sparrow.passport.openapi.title |
Sparrow Community | 文档标题 |
sparrow.passport.openapi.description |
Sparrow Community | 文档描述 |
sparrow.passport.openapi.version |
1.0 | 版本号 |
sparrow.passport.openapi.contact.name |
harry | 联系人 |
sparrow.passport.openapi.contact.url |
http://www.sparrowzoo.com | 联系人主页 |
sparrow.passport.openapi.contact.email |
zh_harry@163.com | 联系人邮箱 |
7.3 starter 隔离机制
@Import+@EnablePassport:非隐式自动装配,宿主必须显式@EnablePassport才生效@ConditionalOnMissingBean(OpenAPI.class):宿主自定义了 OpenAPI Bean 时,starter 自动让位@ConditionalOnProperty(...enabled):可用sparrow.passport.openapi.enabled=false一键关闭- 独立前缀
sparrow.passport.openapi.*:与官方springdoc.*及宿主自定义零冲突
application.properties 中的 knife4j.enable /
knife4j.production 是旧 knife4j 的遗留项,在 springdoc 下已失效,
应替换为 springdoc.api-docs.enabled / springdoc.swagger-ui.enabled。
08分组(GroupedOpenApi)—— 按应用隔离接口文档
springdoc 不会自动创建分组,需要通过 GroupedOpenApi Bean 显式声明。
对应关系:knife4j「分组」/ springfox Docket.groupName →
springdoc GroupedOpenApi.group()。定义多个分组后,
Swagger UI 右上角会出现下拉,按应用切换接口文档。
8.1 分组配置项
| 属性 | 默认值 | 说明 |
|---|---|---|
sparrow.passport.openapi.group |
passport | 分组名(下拉中显示) |
sparrow.passport.openapi.packages-to-scan |
com.sparrow.passport | 该分组扫描的包,用于隔离 |
8.2 starter 内的分组 Bean
@Bean
@ConditionalOnMissingBean(name = "passportGroup") // 按名覆盖,允许多分组共存
@ConditionalOnProperty(prefix = "sparrow.passport.openapi", name = "enabled", ...)
public GroupedOpenApi passportGroup(PassportOpenApiProperties properties) {
return GroupedOpenApi.builder()
.group(properties.getGroup())
.packagesToScan(properties.getPackagesToScan())
.build();
}
8.3 多应用共存示例
其它应用各定义一个不同 group 的分组,即可在同一下拉中隔离展示:
@Bean
public GroupedOpenApi fileGroup() {
return GroupedOpenApi.builder()
.group("file")
.packagesToScan("com.sparrow.file")
.build();
}
/v3/api-docs/{group}(例如
/v3/api-docs/passport),需确保该路径在认证放行列表内
(/v3/api-docs/** 已覆盖)。