这两年做 SpringBoot 项目,只要是从 SpringBoot 2 往 3 升级的团队,十有八九都会在接口文档这块栽一遍。SpringBoot 3 正式把javax换成了jakarta命名空间,SpringFox 直接停更不兼容,以前那套springfox-boot-starter+ Swagger 2 的写法在 SpringBoot 3 里根本起不来,页面直接 404,控制台一堆 NoClassDefFoundError。这个时候 SpringDoc OpenAPI 就成了最稳的替代方案,它原生支持 OpenAPI 3 规范,并且专门出了springdoc-openapi-starter-webmvc-ui给 SpringBoot 3 用。这篇文章我就围绕 SpringBoot 3 集成 SpringDoc OpenAPI 这套组合,把依赖引入、注解声明、JWT 鉴权放开、生产环境关闭、分组配置、排障技巧这些全部过一遍,所有内容都是我实际项目和升级过程中踩过坑之后总结下来的,可以直接照着抄。
这套方案适合谁?Backend 开发、负责项目脚手架搭建的同学,还有那些正在从 SpringBoot 2 升级到 3、或者项目里还在为接口文档和前端扯皮的人。不需要你对 OpenAPI 规范有多深的理解,按照下文的步骤配一遍就能跑起来。
1. 为什么 SpringBoot 3 要做接口文档,以及为什么选 SpringDoc
1.1 从接口协作的实际痛点说起
现在稍微正规一点的团队,前后端分离基本是标配。前端拿着接口文档开发页面,后端按照文档实现接口,理论上职责明确,但实际协作里文档往往是最先腐烂的东西。接口改了参数没更新文档、新增字段没同步,前端照着旧文档联调,一调一个不通过,最后只能靠人肉沟通,效率极低。
自动化接口文档的价值就在这里:接口代码本身是事实来源,通过注解和 OpenAPI 规范把接口的请求参数、响应结构、鉴权方式、错误码全部描述清楚,再渲染成 Swagger UI 页面,文档和代码永远保持一致。只要后端改了注解或签入新的 DTO,重新启动项目,文档立即同步更新,不存在“代码改了文档忘了改”的问题。
SpringBoot 3 出来后,这个问题变得格外扎眼。SpringFox 从 2020 年左右就基本停止维护了,它内部强依赖 javax 命名空间,SpringBoot 3 全面转向 jakarta 之后,ClassNotFound 和 NoSuchMethodError 直接炸穿。我见过很多人升级 Boot 3 之后还在硬试 springfox,浪费一两天在这个死胡同里,属实不值得。
1.2 为什么弃用 SpringFox,选择 SpringDoc
SpringDoc 不是简单换了个包名,它在架构层面就是针对 OpenAPI 3 规范重新实现的。对比 SpringFox 的优势,我最看重以下几点。
第一,原生支持 SpringBoot 3 和 jakarta。SpringDoc 从 2.x 开始提供 starter 模块,明确适配 SpringBoot 3 和 jakarta servlet 环境,依赖名就写得很清楚:springdoc-openapi-starter-webmvc-ui。如果用 WebFlux 响应式项目,还有对应的springdoc-openapi-starter-webflux-ui。这一点对新人来说极大降低了选型难度,不用担心版本系列搞混。
第二,注解风格贴合 OpenAPI 3。OpenAPI 3 里用@Operation、@ApiResponse、@Parameter、@Schema这套注解,比旧版 Swagger 2 的@ApiOperation、@ApiImplicitParam语义更明确。它和 swagger-core 2.x 系列同源,只要是写 OpenAPI 3 注解,将来如果要迁移到别的工具链,注解基本不用动。
第三,分组和配置更灵活。SpringDoc 支持基于包路径的GroupedOpenApi分组,也可以直接在application.yml里配置分组规则。比如把用户端接口、管理端接口分成不同文档,前端只需要看自己那部分,不会整个项目几百个接口全堆在一起,体验非常好。
第四,对 Spring Security、OAuth2、JWT 的支持很完善。你可以通过配置给文档注入 Bearer Token 认证,Swagger UI 上直接填 Token 就能调试接口,这一点在老项目对接登录鉴权时尤其好用,后面我会专门演示。
2. 基础环境与依赖集成
2.1 环境要求与版本对应关系
先说版本这一块,这是新手最容易踩坑的地方。SpringBoot 3 要求 Java 17 起步,所以你的 JDK 至少是 17,我这边生产环境用的是 Java 21 LTS,配合 SpringBoot 3.2 和 3.3 都跑得很稳。Maven 或 Gradle 只要支持对应 Java 版本就行,Maven 3.6.3 以上建议直接 3.9 系列,避免一些插件兼容问题。
SpringDoc 的版本选择有个公式:SpringBoot 3 就用 springdoc-openapi-starter-webmvc-ui 的 2.x 系列最新版。不要拿 1.x 的包去配 Boot 3,那是给 SpringBoot 2 准备的,包名还叫springdoc-openapi-ui,两者不是一回事。
我常用的版本组合给个参考表格:
| SpringBoot 版本 | Java 版本 | SpringDoc 推荐版本 | 使用的 starter 依赖名 |
|---|---|---|---|
| 3.0 / 3.1 | 17+ | 2.1.x / 2.2.x | springdoc-openapi-starter-webmvc-ui |
| 3.2 / 3.3 | 17+ | 2.3.x / 2.5.x | springdoc-openapi-starter-webmvc-ui |
| 3.4+ | 17+ | 2.6.x / 2.7.x | springdoc-openapi-starter-webmvc-ui |
提示:具体小版本号建议去 Maven Central 或项目 GitHub Releases 页面确认最新版,不要把这个表格当成死参数,我的习惯是隔一段时间看一眼官方仓库的 release notes。
2.2 Maven 与 Gradle 依赖配置
Maven 项目在pom.xml里加一段依赖就行:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.7.0</version> </dependency>这个依赖会自动把 springdoc 的核心模块、swagger-ui 前端资源、以及 OpenAPI 3 相关的序列化库带进来,你不用再额外引入 swagger-annotations 那一堆东西。如果你用的 Gradle,对应写法:
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'加完之后,启动项目,浏览器访问http://localhost:8080/swagger-ui.html,正常情况下就能看到 Swagger UI 页面了。如果项目配了 context-path,路径也要对应加上。SpringDoc 的默认 API 文档 JSON 路径是/v3/api-docs,UI 会去读这个地址的 JSON,然后渲染成页面。
我第一次集成的时候,光看到空页面没有接口,第一反应以为是依赖没生效,后来才发现是 Controller 还没写,或者接口路径不在默认扫描范围内。所以集成完先写一个最简单的 Controller 验证路径,再看 UI 内容,这样能更快定位问题。
3. 声明式 API 文档开发:从注解到 OpenAPI 规范
3.1 核心注解详解与代码示例
SpringDoc 的文档信息是通过注解一条条声明出来的。项目全局信息用@OpenAPIDefinition配置,接口级信息用@Operation、@ApiResponse配置,模型字段用@Schema描述。下面是我平时写 Controller 的样板:
@Tag(name = "用户管理", description = "用户模块相关接口") @RestController @RequestMapping("/api/user") public class UserController { @Operation(summary = "创建用户", description = "新增一条用户记录,邮箱和手机号二选一") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "创建成功"), @ApiResponse(responseCode = "400", description = "参数校验失败") }) @PostMapping public ApiResult<UserVO> createUser(@RequestBody @Valid UserCreateRequest request) { return ApiResult.success(userService.create(request)); } }对应地,请求和返回的 DTO 上加上@Schema描述字段含义:
@Schema(description = "创建用户请求") public class UserCreateRequest { @Schema(description = "用户名", example = "zhangsan") @NotBlank(message = "用户名不能为空") private String username; @Schema(description = "邮箱", example = "zhangsan@example.com") @Email private String email; @Schema(description = "手机号", example = "13800138000") private String phone; }这里有几个点需要特别注意。@Schema的example属性最好写上,因为 Swagger UI 在用到 Example Value 的时候,有 example 比没 example 更直观,前端可以直接抄。@Operation的summary是接口列表页显示的标题,description是点进去之后的详细说明,建议 summary 写短、description 写全。
另外,泛型返回结构如ApiResult<T>在 OpenAPI 3 里也能正确展开,但前提是你别把泛型擦除得太严重。比如ApiResult<UserVO>这种写法,SpringDoc 能通过反射拿到 UserVO 类型,从而把返回结构完整渲染出来。如果你在方法返回值里写ApiResult不带泛型,文档里就只能看到一个空壳对象。
3.2 响应模型、分页、枚举等细节处理
除了基础注解,实际项目里最常见的几个场景是分页参数、枚举字段、日期时间格式。这些处理不好,文档会显得很“业余”,前端也会频繁来问。
先说分页。Spring Data 的Pageable在 SpringDoc 中能自动展开为 page、size、sort 等查询参数,但需要引入依赖配合,如果你用的是 Spring MVC + MyBatis 写手分页,建议在@Parameter注解里手动描述参数,比如:
@Operation(summary = "分页查询用户") public ApiResult<PageResult<UserVO>> pageUser( @Parameter(description = "页码,从0开始") @RequestParam(defaultValue = "0") int page, @Parameter(description = "每页大小") @RequestParam(defaultValue = "10") int size ) { ... }枚举字段建议在@Schema里用allowableValues列出可选值。比如状态字段:
@Schema(description = "用户状态", allowableValues = {"ENABLED", "DISABLED", "LOCKED"}) private UserStatus status;这样 UI 下拉框就能看到可选枚举值,前端取值的时候不会瞎猜。日期时间字段建议统一用@JsonFormat或者全局 Jackson 配置,比如yyyy-MM-dd HH:mm:ss,避免文档里展示出一大串 ISO 时间戳,联调时容易对不上格式。我踩过一次坑,后端存的是 LocalDateTime,Jackson 默认序列化带 T 和时区信息,文档里显示出来完全不是前端想要的样子,前端还以为接口返回错了。后来在application.yml里统一配置了日期格式,所有接口的行为才一致。
4. 与 Spring Security 集成:JWT 鉴权下的 Swagger 文档
4.1 让 Swagger UI 在鉴权环境中可访问
如果你的项目里有 Spring Security,那 Swagger UI 默认会被拦截器挡在门外,访问时会出现 401 或登录重定向。更麻烦的是,就算你放行了 UI 页面,UI 内部去请求/v3/api-docs接口时如果也被拦截,页面依然渲染不出来。
解决办法是在 SecurityConfig 的过滤链里放行这些端点。注意要放行的是 swagger-ui 静态资源和 v3/api-docs 动态接口,不是把所有请求都放开。示例配置:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers( "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html" ).permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); }如果你的 springdoc 配置了额外的 path,比如/openapi/**,记得把那个路径也加上。这里有个小细节:Spring Security 6 使用requestMatchers而不是旧的antMatchers,写法别搞混了。
4.2 JWT 凭证注入 OpenAPI UI
光放行还不够,因为你的业务接口大多是要带 Token 的。如果 Swagger UI 上没法填 Token,前端同事打开文档调试时,每个接口都要自己构造 Authorization 头,体验很糟糕。SpringDoc 支持配置全局安全方案,让 UI 页面上出现一个 Authorize 按钮,填一次 Token,之后所有请求自动带上。
先定义一个OpenAPIBean,注册 Bearer Token 安全方案:
@Configuration public class OpenApiConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title("XXX系统 API 文档") .version("v1.0.0") .description("用户、订单、支付模块接口说明")) .components(new Components() .addSecuritySchemes("bearer-jwt", new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT"))) .addSecurityItem(new SecurityRequirement().addList("bearer-jwt")); } }addSecurityItem放在全局,表示所有接口默认都需要这个 Bearer Token。如果你想对部分接口放行,比如登录接口,可以在对应的 Controller 方法上加上:
@Operation(summary = "登录") @SecurityRequirements(value = {}) // 覆盖全局安全要求,等价于不需要token @PostMapping("/login") public ApiResult<LoginVO> login(@RequestBody LoginRequest request) { ... }这里最好用 SpringDoc 提供的@SecurityRequirements注解而不是直接吞掉所有错误,逻辑更清晰。
配置好之后,Swagger UI 右上角会多一个 Authorize 按钮,点进去输入token值,后续所有请求头都会自动带Authorization: Bearer xxx。很多前端第一次看到这个功能都很惊喜,说不用再去 Postman 里单独配 Header 了。
注意:实际项目里要小心不要在
OpenAPIBean 里写死 Token 或任何敏感信息,这个配置只负责声明安全方案,真正的登录获取 Token 流程还是要走你们自己的认证接口。
5. 生产环境策略与文档分组
5.1 分组管理多模块接口
项目稍微大了以后,后端接口分成多个模块,比如用户、订单、支付、后台管理。如果全部接口混在一个文档里,前端打开页面几百个接口,找一条接口要翻半天。SpringDoc 分组功能就是解决这个问题的。
分组有两种配置方式。第一种,在application.yml里用springdoc.group-configs声明:
springdoc: group-configs: - group: user-api packages-to-scan: com.example.user paths-to-match: /api/user/** - group: admin-api packages-to-scan: com.example.admin paths-to-match: /api/admin/**第二种,用代码方式定义GroupedOpenApiBean:
@Bean public GroupedOpenApi userApi() { return GroupedOpenApi.builder() .group("用户端接口") .packagesToScan("com.example.controller.user") .pathsToMatch("/api/user/**") .build(); }两种方式效果差不多,我更喜欢 yml 配置,因为不用改代码就能调分组,发测试环境时改一行路径就行。分组之后,Swagger UI 左上角会出现下拉框,可以切换不同组的文档。每个分组会生成独立的 OpenAPI JSON 路径,比如/v3/api-docs/user-api,方便后续接入其他工具。
5.2 生产环境关闭文档与审计
很多团队会忽略一件事:Swagger UI 直接在生产环境暴露,等于把全部接口的路径、参数、数据模型免费送给攻击者,安全上是很大的风险。我见过不止一个公司,生产环境甚至能直接访问/swagger-ui.html,这是一个非常危险的信息泄露入口。
SpringDoc 提供了开关,可以在生产环境关闭文档功能:
springdoc: api-docs: enabled: false swagger-ui: enabled: falseapi-docs.enabled控制/v3/api-docsJSON 输出,swagger-ui.enabled控制 UI 页面。两个都关掉之后,访问对应地址返回 404。但这里有个问题:如果配置是写在同一个application.yml里,开发环境也没文档看了,体验很差。
正确做法是把配置拆到 profile 里。比如application-prod.yml里单独关闭:
# application-prod.yml springdoc: api-docs: enabled: false swagger-ui: enabled: false开发环境用默认配置不写这两项,本地启动照样有文档;测试环境如果要给前端做 mock,也保持开启。等到生产部署时再把 profile 切到 prod。如果公司有配置中心,也可以把这两项做成动态开关,运维出故障时临时开启排查,不过要加一层权限控制,不能裸奔。
分组和开关这两个配置看起来只是几行 yml,但放到项目里就是开发体验和安全底线之间的平衡。我在几个项目里实践下来,确认这个策略能减少很多不必要的联调摩擦,也让安全审计少一个大漏洞。
6. 常见问题与排障实录
6.1 版本冲突与类加载问题
我碰到最多的一个问题,就是项目里同时存在springfox和springdoc依赖,导致启动时报一堆类冲突或者注解不生效的怪问题。迁移到 SpringDoc 时,第一步就是把 SpringFox 的所有依赖完整移除,包括springfox-boot-starter、springfox-swagger2、springfox-swagger-ui,然后执行mvn clean清掉旧编译产物。千万不要以为只改一个版本号就能共存,SpringFox 和 SpringDoc 都会尝试生成 OpenAPI 文档,两个 Bean 竞争同一个资源,行为不可控。
另一个常见问题在 SpringBoot 3.0 早期版本,有可能出现ClassNotFoundException: javax.validation.ConstraintViolationException这种。原因是部分老依赖还在引用 javax.validation,而 Boot 3 里已经从 javax 切换为 jakarta.validation。这时候要检查项目里有没有老版本的validation-api依赖,改成:
<dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> </dependency>或者直接引入spring-boot-starter-validation这个 starter,它会自动帮你选匹配的版本。
如果你在升级过程中把 Boot 从 3.0 升到 3.2,SpringDoc 也从 2.0.x 升到 2.2.x,可能出现NoSuchMethodError,多半是 Swagger UI 前端资源和 SpringDoc 后端模块版本不一致。解决办法就是对齐版本,别混合使用不同小版本的 jar 包。
6.2 页面加载不出、扫描不到接口等实际问题
Swagger UI 页面能打开,但接口列表是空的,这是另一个高频问题。背景原因基本可以排查这几个方向:
第一,Controller 所在包有没有被 SpringDoc 扫描到。SpringDoc 默认扫描 Spring 容器的所有 controller,如果你用@RestController但没被 Spring 管理,那肯定扫不到。这种情况通常会伴随 Spring 启动日志里有 Controller 映射,但 API 文档为空,先检查 Controller 类上是不是漏了@RestController或@Controller。
第二,路径匹配问题。如果你在 yml 里配了paths-to-match,但 Controller 的请求路径不在匹配范围内,也会扫不到接口。比如paths-to-match配了/api/**,而 Controller 路径是/rest/user,那就不会出现在文档里。可以先用 curl 看 JSON 里有没有接口:
curl http://localhost:8080/v3/api-docs如果输出里没有你的接口路径,说明路径匹配确实有问题,调整一下规则。
第三,404 问题。UI 打不开但是/v3/api-docs能访问,多半是安全配置把/swagger-ui/**挡了。按第 4.1 节的配置放行即可。如果/v3/api-docs也 404,先确认 springdoc 依赖是否真的被加载了。可以把springdoc.api-docs.enabled显式设为 true,再用一个简单 Controller 验证基本功能。
第四,没有 show 出字段或者 DTO 属性缺失。这种情况通常出在 getter 缺失或 Jackson 序列化配置上。SpringDoc 依赖 Jackson 来解析模型,如果 DTO 没有标准的 getter 方法,字段就没法被识别。另外,类上加了@JsonIgnoreProperties或者字段上有@JsonProperty(access = Access.WRITE_ONLY)也可能导致文档视图中字段缺失。调整 DTO 的 getter 和注解后,刷新页面即可。
还有一个不算问题的点:如果你的项目里用了 swagger 注解的@ApiModelProperty,在 SpringDoc 3 的注解体系里已经改成@Schema(description = "..."),老注解虽然在某些场景下能生效,但功能不完整。迁移的时候建议全局搜一遍,把所有io.swagger.annotations.*的引用替换成io.swagger.v3.oas.annotations.*的注解。这步工作量大,但做一次以后文档质量稳定很多。
最后说个我自己的习惯:遇到文档相关的问题,先不要急着翻源码,先用/v3/api-docs拿 JSON 对照 UI 页面。JSON 有数据但 UI 空白,那就是前端渲染问题;JSON 本身就没有数据,那才需要追后端配置。这个排查思路能帮你快速缩小问题范围,省下很多时间。