SpringBoot 3.x一出来,“Swagger还能不能用了”就成了Java后端群里反复出现的灵魂拷问。很多人照旧把springfox 2.9.2往pom里一放,启动直接报错,或者swagger-ui.html打死都是404,折腾半天以为是端口写错了、路径配岔了、缓存没清,其实是这个老家伙在SpringBoot 3.x底下已经彻底活不下去了。
这篇文章就是聊明白一件事:SpringBoot 3.x时代,Swagger文档到底怎么整合才能不踩坑。我会从框架选型讲到Maven依赖、配置项、注解迁移、生产环境安全漏洞修复,最后附一份常见问题速查表。不管是刚入门还是从Boot 2.x老项目往上迁的,照着做基本一个下午能搞定。
1. SpringBoot 3.x 为什么把Swagger卡死了
1.1 javax调包成jakarta,这可不是版本号升级那么简单
Spring Boot 3.0底层换成了Spring Framework 6,Java EE的命名空间从javax.*迁移到了jakarta.*。说得直白一点,所有用javax.servlet、javax.validation的老代码,在Boot 3下全都跑不了,因为整个运行环境已经没有这些类了。
springfox的2.x和3.0.0都是用javax写死的老代码,所以你把springfox-boot-starter装进Boot 3工程,启动时大概率会看到一堆NoClassDefFoundError或者ClassNotFoundException,源头就是这里。这不是配置能救的,光调yml、改路径一点用没有。
很多人在群里问“SpringBoot版本太高怎么办”,说的其实都是这个问题。实际不是版本高,是生态断层了。Boot 2.7是javax的终点站,Boot 3.0开始全是jakarta的地盘,老框架没跟着改就淘汰了。
1.2 springfox基本停更,接棒的是springdoc-openapi
springfox最后一次像样的更新已经过去很久了,连OpenAPI 3规范的支持都很勉强。Boot 2.6开始调整Spring MVC路径匹配策略的时候,springfox就折腾了很久才适配。到了Boot 3这种结构性变更,它已经跟不上了。
现在SpringBoot 3.x整合Swagger的主流方案是springdoc-openapi。这个项目一直在跟随Spring Boot版本迭代,Boot 3.x用springdoc-openapi-starter-webmvc-ui2.x,Boot 4.x用springdoc-openapi3.x,版本对应关系非常清楚。
还有个选择是国产的knife4j。4.x版本已经切换成基于springdoc-openapi来做增强,界面更符合国内团队的习惯,提供了更丰富的OpenAPI增强功能。如果团队之前用的是knife4j,那直接迁到4.x即可,底层思路和springdoc一致。
1.3 版本对应关系先搞清楚,后面少走弯路
先上一个版本对应的速查表,这是我在实际项目中反复核对过的:
| Spring Boot版本 | 推荐依赖组 | 推荐版本 |
|---|---|---|
| 3.0.x - 3.2.x | org.springdoc:springdoc-openapi-starter-webmvc-ui | 2.2.0 - 2.3.0 |
| 3.3.x | 同上 | 2.5.0 - 2.6.0 |
| 3.4.x | 同上 | 2.6.0 - 2.8.0 |
| 4.x(目前) | org.springdoc:springdoc-openapi-webmvc-ui | 3.x |
下面的例子我统一用SpringBoot 3.4 +springdoc-openapi2.6.0来演示,这也是目前实测下来最稳的一套组合。
2. 动手实操:SpringBoot 3.x整合springdoc-openapi
2.1 Maven依赖就加这一个,别的都是多余
先加核心依赖。pom.xml里这样写:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.6.0</version> </dependency>需要注意,这个starter已经包含了Swagger UI和OpenAPI核心模块,不需要再额外引入springdoc-openapi-core之类的包。如果项目用的是WebFlux,就把webmvc换成webflux;如果项目是微服务架构,网关层建议只引入springdoc-openapi-starter-common,由下游服务各自暴露文档,网关统一聚合。
这里有个小细节:依赖里不要混着再引springfox,新旧两套框架同时存在会互相干扰,特别是在Spring MVC的路径匹配和文档Bean初始化阶段,容易出现奇奇怪怪的冲突。
2.2 最简单的配置:不写Bean都能跑
springdoc-openapi的好处是,默认配置下加完依赖就能访问。启动项目后,浏览器访问以下地址:
- Swagger UI页面:
http://localhost:8080/swagger-ui.html - OpenAPI JSON接口:
http://localhost:8080/v3/api-docs
/swagger-ui.html实际上会重定向到/swagger-ui/index.html,所以两个地址都能用。/v3/api-docs是后端自动生成的OpenAPI规范JSON,UI页面就是靠解析这个JSON渲染出来的。
如果要修改默认路径或者开关状态,在application.yml里配置:
springdoc: api-docs: enabled: true path: /v3/api-docs swagger-ui: enabled: true path: /swagger-ui.html packages-to-scan: - com.demo.controllerpackages-to-scan可以用来限定扫描范围,避免把不该暴露的内部接口全部扫进文档。这个配置在大型项目里非常实用,我一般都会显式指定Controller所在包。
2.3 用OpenAPI Bean配置文档元信息
依赖加上、页面能打开,只是第一步。要让文档看起来像一个正经项目的接口文档,还需要配置标题、描述、版本号、联系人信息等元数据。这比在Controller注解里写一大堆description要方便得多,也更便于维护。
新建一个配置类:
package com.demo.config; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class OpenApiConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title("用户中心服务 API") .description("用户中心服务接口文档,包含用户管理、权限管理等模块。") .version("v1.0.0") .contact(new Contact() .name("后端团队") .email("backend@example.com")) .license(new License() .name("内部使用") .url("https://example.com"))); } }这段代码配置完成后,Swagger UI页面左上角就会显示标题、描述和版本号,接口文档的“门面”就有了。OpenAPI是springdoc提供的模型类,直接new出来注入即可,不需要再去继承什么乱七八糟的配置类。
提示:
/v3/api-docs返回的JSON里,info字段的内容就是这里配置的。前端如果通过OpenAPI生成TypeScript客户端,也会读取这里的描述信息。
3. 注解使用:从Springfox迁移到OpenAPI 3
3.1 @Api变成@Tag,@ApiOperation变成@Operation
老项目用springfox时,Controller上挂的是@Api,方法上挂的是@ApiOperation。到了springdoc,这些注解换成了@Tag和@Operation,包路径也变了,从io.swagger.annotations换成了io.swagger.v3.oas.annotations。
对照关系如下:
| Springfox注解 | springdoc注解 | 作用位置 |
|---|---|---|
@Api(tags = "用户接口") | @Tag(name = "用户接口", description = "用户相关接口") | 类 |
@ApiOperation("查询用户") | @Operation(summary = "查询用户", description = "根据ID查询用户信息") | 方法 |
@ApiParam(value = "用户ID") | @Parameter(name = "id", description = "用户ID") | 方法参数 |
@ApiModel | @Schema | 实体类 |
@ApiModelProperty | @Schema(description = "用户名") | 实体字段 |
@ApiIgnore | @Hidden | 类/方法/参数 |
Controller代码示例:
package com.demo.controller; import com.demo.entity.User; import com.demo.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.*; @Tag(name = "用户接口", description = "用户相关接口") @RestController @RequestMapping("/api/user") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @Operation(summary = "查询用户", description = "根据用户ID查询用户详情信息") @GetMapping("/{id}") public User getUser(@PathVariable("id") Long id) { return userService.getById(id); } }实体类用@Schema描述字段含义:
package com.demo.entity; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "用户实体") public class User { @Schema(description = "用户ID", example = "1") private Long id; @Schema(description = "用户名", example = "zhangsan") private String username; // getter/setter 省略 }3.2 参数名变成arg0、arg1的坑
整合完成后,细心的同学会发现一个问题:有些接口文档里的参数名变成了arg0、arg1,或者干脆显示不出来。原因在于Java编译时默认不保留参数名信息,反射拿不到真实的名字。
解决办法有两种:
第一种,Maven编译时开启-parameters参数,推荐在生产项目里统一开启。在pom.xml的<build>节点里添加:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <parameters>true</parameters> </configuration> </plugin> </plugins> </build>第二种,在每个参数上显式加@Parameter注解:
@Operation(summary = "分页查询用户") @GetMapping("/page") public Result<PageResult<User>> page( @Parameter(name = "pageNum", description = "页码", example = "1") int pageNum, @Parameter(name = "pageSize", description = "每页条数", example = "10") int pageSize) { return userService.page(pageNum, pageSize); }个人推荐两种结合:全局开启-parameters,参数级描述用@Parameter补充。这样既能保证参数名正确,又能让文档展示更友好。
3.3 多模块分组:一个项目多个文档页面
微服务或者多模块项目里,所有接口堆在一个文档里会非常难用。springdoc支持分组配置,可以按包名、路径或注解来拆分。
一种是yml方式:
springdoc: group-configs: - group: user-api packages-to-scan: com.demo.user.controller - group: order-api packages-to-scan: com.demo.order.controller另一种是Bean方式,在配置类里声明:
@Configuration public class OpenApiConfig { @Bean public GroupedOpenApi userApi() { return GroupedOpenApi.builder() .group("user-api") .packagesToScan("com.demo.user.controller") .build(); } @Bean public GroupedOpenApi orderApi() { return GroupedOpenApi.builder() .group("order-api") .packagesToScan("com.demo.order.controller") .build(); } }配置完成后,Swagger UI右上角会多出一个下拉框,可以在不同分组之间切换。使用Bean方式还能灵活添加pathsToMatch、pathsToExclude等过滤规则,比yml方式更好控制。
4. Swagger未授权访问漏洞:整合之后的安全必修课
4.1 这个漏洞到底是怎么来的
很多安全扫描工具在扫描SpringBoot项目时,都会报一个“Swagger API未授权访问漏洞”,原理扫描显示“可验证”。意思是:扫描器发现/swagger-ui.html或/v3/api-docs这些接口在没有登录的情况下可以直接访问,而且返回的内容里含有明显的Swagger标识和完整的接口定义。
有人觉得“接口文档暴露了而已,能有多大问题”。实际上,一份完整的OpenAPI JSON相当于把整个项目的家底盘了一遍:所有路径、请求参数、返回结构、字段含义、枚举值,全都摆出来了。攻击者不需要逆向分析,直接照着文档找越权接口、未校验参数、敏感数据接口,成功率高得可怕。
被报“未授权访问”的真正原因,是项目在部署时没有把接口文档的生产访问权限控制住。开发环境开文档没有问题,生产环境还裸奔着就是事故。
4.2 不同环境差异化管控:生产环境直接关掉
最安全的处理方式,是生产和开发环境分开配置。开发环境开启文档方便调试,生产环境直接关闭,从根源上杜绝暴露。
application-dev.yml:
springdoc: api-docs: enabled: true swagger-ui: enabled: trueapplication-prod.yml:
springdoc: api-docs: enabled: false swagger-ui: enabled: false这样线上环境访问/swagger-ui.html和/v3/api-docs都会返回404,安全扫描自然通过。
但这种方案有个限制:如果公司内部有联调环境、测试环境,也需要看文档,那就不能一刀切关闭,而是需要给文档接口加上访问权限。
4.3 Spring Security保护文档端点
如果你用了Spring Security,正确的做法是将Swagger相关端点纳入权限管理,而不是直接放行。经常看到网上有人教你“这些路径全部放行,不然UI打不开”,这种搞法等于自己把门拆了。
SpringBoot 3.x + Spring Security 6.x下,配置类不再继承WebSecurityConfigurerAdapter,而是通过SecurityFilterChainBean来定义。示例:
package com.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").hasRole("ADMIN") .anyRequest().authenticated() ) .formLogin(login -> login.permitAll()) .httpBasic(basic -> basic.permitAll()); return http.build(); } }这样配置后,/swagger-ui.html和/v3/api-docs就需要具备ADMIN角色才能访问。开发环境如果不想走登录,可以单独为devprofile放行:
@Profile("dev") @Bean public SecurityFilterChain devFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()); return http.build(); }注意:Spring Security 6.x里
requestMatchers已经替代了旧版的antMatchers。如果项目里还有老写法,启动时会直接报错。同时,Swagger UI内部的静态资源路径都需要放行,否则页面能打开但里面的JS/CSS加载不了,UI会白屏。
4.4 heapdump敏感信息泄露:不能只管Swagger
热词里还有个高频漏洞是“springboot heapdump 敏感信息泄露”。这个和安全扫描的逻辑同源:SpringBoot Actuator如果暴露了heapdump端点,攻击者可以直接下载堆转储文件,用工具解析就能提取出数据库密码、Redis密码、Token等内存里的敏感信息。
很多项目Swagger没暴露,但Actuator的heapdump、env、beans端点被人扫出来了。修复思路和环境隔离一致:
management: endpoints: web: exposure: include: health,info万一确实需要在线排查问题,至少给/actuator/**加上和Swagger一样的访问控制,或者限制只能内网IP访问。同时,配置里的密码、密钥等敏感信息,尽量用环境变量注入,不要硬编码在application.yml里,这样即便堆转储被下载,能挖到的关键信息也有限。
4.5 安全扫描报告“可验证”之后怎么处理
如果安全团队给了一份扫描报告,里面标注Swagger“原理扫描可验证”,处理节奏建议是这样:
- 先确认漏洞环境,访问
/swagger-ui/index.html是否确实能打开。 - 如果生产环境不依赖在线文档,直接按4.2的方案把
springdoc.api-docs.enabled和springdoc.swagger-ui.enabled设为false,重启后复测。 - 如果生产环境必须开文档,走4.3的方案给文档端点加权限认证。
- 复测时除了
/swagger-ui.html和/v3/api-docs,还要检查/swagger-ui/index.html、/doc.html(如果用了knife4j)以及分组后的/v3/api-docs/下的子路径。
经验之谈:有的团队只给网关做了放行,绕过了网关直接用内网地址访问照样能打开。真正保护好文档,得在服务端层面控制,不能依赖网关单点拦截。
5. 常见问题与排查技巧实录
5.1 访问/swagger-ui.html一直404
这个问题排在所有整合问题里的第一位。排查顺序建议如下:
- 确认依赖是
springdoc-openapi-starter-webmvc-ui而不是springfox。混入了springfox相关依赖会干扰路径映射。 - 确认Spring Boot版本和springdoc版本匹配。Boot 3.4配springdoc 2.2.0大概率有问题,换成2.6.0再试。
- 确认项目有没有配
server.servlet.context-path。如果有这个前缀,访问地址也要加上,比如http://localhost:8080/myapp/swagger-ui.html。 - 确认Spring Security有没有拦截。即使你没写SecurityConfig,如果引入了
spring-boot-starter-security,默认所有路径都需要认证,UI自然也进不去。看一眼控制台有没有生成默认密码。
5.2 页面能打开,但接口列表是空的
打开Swagger UI后,一片空白,没有扫描到任何Controller。多数情况是packages-to-scan配置有误,或者Controller类上没有加@Tag注解。springdoc默认扫描@RestController注解的类,但如果你显式设置了packages-to-scan,就必须确保包路径写对,否则什么都扫不到。
另外,如果Controller继承了某个基类,而基类和子类都在不同的包里,有时候会漏扫到父类中的接口。这时候要么把接口方法重写在子类中,要么调整包的扫描范围。
5.3 接口能显示,但返回的JSON里字段顺序混乱
@Schema注解的字段默认按Java反射顺序输出,有些项目中实体类字段顺序和前端期望的顺序不一致。解决办法是在实体字段上通过@Schema(description = "...", example = "...")统一描述,同时可以在OpenAPI配置里指定json的序列化顺序。但说实话,接口文档字段顺序对JSON本身没有影响,前端开发真正依赖的是字段名和类型,这个不用过于纠结。
5.4 接口文档里的时间格式显示不一致
如果项目里用的是java.time.LocalDateTime,并且Jackson没有配置全局时间格式,UI上会显示一串数组格式的日期(比如[2025, 6, 15, 14, 30, 25]),非常不直观。
在application.yml里统一Jackson配置可以解决:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8注意,LocalDateTime的序列化不完全受spring.jackson.date-format控制,如果配置后UI仍然显示数组,就需要额外配置JavaTimeModule的序列化器,或者使用@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")标注字段。
5.5 网上教程里的Old Swagger配置不要直接抄
最后提醒一句,网上大量“SpringBoot整合Swagger”的教程还是Boot 2.x + springfox的老写法,照着抄到Boot 3.x项目里必炸。看到springfox、@EnableSwagger2、WebMvcConfigurer里重写addResourceHandlers放行swagger-resources这类内容,直接跳过就行。那些是老时代的产物,在SpringBoot 3.x底下不仅没用,还会拖慢启动速度。
写在最后的一点经验
SpringBoot 3.x整合Swagger,本质上就是一次生态迁移:依赖从springfox换成springdoc-openapi,注解从io.swagger.annotations换成io.swagger.v3.oas.annotations,安全策略从“开了再说”换成“按环境按权限控制”。我实际迁移过几个上百个接口的老项目,最快的那个一下午就搞定,关键就是依赖换干净、版本对应表先查好、再全量替换注解。
再分享一个小技巧:老项目注解批量替换,用IDEA的正则替换。把@Api(value = "xxx")替换成@Tag(name = "xxx"),把@ApiOperation("xxx")替换成@Operation(summary = "xxx"),几分钟就能把几十个Controller的注解全部改完,比手动一个个改省太多时间了。做完记得再跑一遍接口走查,重点看有没有标签丢失、参数名变成arg0这类小问题。这套流程跑通之后,Boot 3.x的接口文档基本就稳了。