Spring Boot 3.x接口文档:springdoc-openapi配置与排坑

Spring Boot 3.x接口文档:springdoc-openapi配置与排坑 Spring Boot 3.x 引入 springdoc-openapi 做接口文档从依赖、配置到排坑一篇讲透如果你最近把项目从 Spring Boot 2.x 升级到 3.x大概率会在接口文档这件事上卡一下。老牌选手 springfox 停更很久Spring Boot 3 全面转向 Jakarta EEspringfox 跑不起来看起来只能硬头皮去找替代品。这个时候大家提到最多的就是 springdoc-openapi 2.x 这个新选择它的 starter 包直接内置了 swagger-ui配合 webmvc-api 模块能在 Spring Boot 3.x 项目里快速生成 OpenAPI 3 规范的文档。这篇就讲清楚它的工作原理、配置步骤、以及我自己实际用下来遇到的坑和解决办法。不管你是刚接触接口文档的新手还是正在迁移老项目的负责人只要你的技术栈是 Spring Boot 3.x Spring MVC我都建议看完这篇文章。它不只是告诉你添加一个依赖那么简单还会带你理解为什么 springdoc 能自动扫出接口、怎么定制分组、怎么和 Spring Security 共存、怎么避免生产环境把接口文档暴露出去。我尽量用项目里真实会遇到的场景来说不讲虚的。1. 为什么 Spring Boot 3.x 时代大家都在选 springdoc-openapi先说背景。Spring Boot 3.x 最大的变化之一就是底层命名空间从 javax 迁移到了 jakarta这时候老牌接口文档工具 springfox 就出了大问题。springfox 2.x 时代依赖的是 Springfox 自己的扫描逻辑对 Spring MVC 的路径匹配方式有很强耦合Spring Boot 2.6 之后路径匹配策略从 AntPathMatcher 改成了 PathPatternParserspringfox 就开始各种不兼容接口扫不全、启动报错、UI 空白都是家常便饭。到了 Spring Boot 3javax.servlet 变成了 jakarta.servletspringfox 直接宣告退役官方也一直没有发布兼容版本。springdoc-openapi 就是在这个窗口期被大家大量使用的替代方案。它的 2.x 版本直接适配了 Spring Boot 3.x底层用 Spring 官方维护的 springdoc 解析器来读取 MVC 的 RequestMappingInfo所以它能拿到非常完整的路由信息。它的核心设计是把 OpenAPI 规范的生成和 Swagger UI 的展示分成两层你加一个依赖就能同时得到 /v3/api-docs 和 /swagger-ui.html 这两个入口。前者是 JSON 数据后者是可视化页面两者通过 springdoc 内置的 swagger-ui 模块关联起来。1.1 springdoc-openapi 2.x 的模块构成springdoc-openapi 2.x 在这个命名上有点绕但它围绕两个维度拆分底层框架是 Spring MVC 还是 Spring WebFluxUI 是使用官方 swagger-ui 还是自己定制。如果你是传统 Servlet 项目引入下面这个 starter 就够了dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version2.6.0/version /dependency这个是 webmvc-api 方案的标准入口。这里的 webmvc-api 就是指 Spring MVC 项目走 OpenAPI 规范生成接口文档它内置了 swagger-ui 的静态资源不需要额外引入前端包。如果你用的是 WebFlux 响应式项目就要换成 springdoc-openapi-starter-webflux-ui。如果你的项目不需要 UI只需要 JSON也可以用 springdoc-openapi-starter-webmvc-api就是这个包里面没有 UI 那部分静态资源访问不了 /swagger-ui.html。实际操作的时候很多人会问为什么 Spring Boot 3 不能直接用 springdoc-openapi-ui这个其实是 1.x 的坐标2.x 改成了 starter-webmvc-ui 这种形式。如果你在 Maven 里搜 springdoc-openapi-ui 还能看到老版本但那个老版本是针对 Spring Boot 2.x 的直接塞进 Spring Boot 3 项目里会报各种 NoSuchMethodError我建议不要踩这个坑。1.2 springdoc 对比 springfox 的核心优势之前用 springfox 的人可能感受过它的配置特别繁琐要写一大堆 Docket Bean 去控制扫描范围还要自己处理响应消息的封装。springdoc 在这一块做了一些简化你甚至不需要配置任何 Bean 就能生成一份能看的文档因为它会默认扫描所有 Controller 和所有 RequestMapping。但当你需要定制分组时它提供了 GroupedOpenApi 这个轻量工具比 springfox 的 Docket 写起来直观很多。另外 springdoc 对 OpenAPI 3 规范的支持是原生级别的而 springfox 3.0 时代虽然也支持 OpenAPI 3但背后还是走的一堆兼容层很多注解的语义对不上。springdoc 直接使用 io.swagger.core.v3 的注解集也就是 Operation、Parameter、Schema 这些这套注解本身就是 OpenAPI 3 的标准 Java 实现所以从注解到 JSON 输出链路非常短不会出现“注解写了但文档里没有”的情况。2. 引入依赖后真正需要改的几个配置项我见过不少人加了依赖就跑起来看了结果发现接口路径不对、分组没生效、字段解释乱七八糟。其实 springdoc 的默认行为够用但真实项目里还是需要做一些配置的尤其是当你使用了统一响应体、全局异常处理、Security 这类常见组件的时候。2.1 application.yml 里最关键的几个配置项springdoc 的配置项非常多但实际项目里最先用到的也就几个。我在自己的配置文件里一般会写上这些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 paths-to-match: - /api/** packages-to-scan: - com.example.demo.controller show-actuator: false先解释 paths-to-match。这个参数控制的是“哪些请求路径会被生成到文档里”。很多项目接口都集中在 /api 下面那就写 /api/**。不写的话就是扫描所有路径包括 error 接口和一些 Spring Boot 自动配置暴露出来的端点看着特别乱。再看 packages-to-scan。这个是限定要扫描哪个包下的 Controller如果你没写 Controller 之外的接口类这个参数可以省略。我建议还是写明确一点因为真实项目里 Controller 会分布在多个模块你可以写多个包路径。不写的话 springdoc 会扫描应用主类所在的包及其子包一般也能覆盖到但如果你把 Controller 放在一个独立 jar 包里建议还是显式写出来。display-request-duration 是一个容易被忽略但很实用的小配置。打开之后Swagger UI 的接口列表里会显示每个接口的实际请求耗时这个在本地联调和压测观察时特别好用。operations-sorter 我习惯改成 method这样接口会按照 GET、POST、PUT、DELETE 来排序工整很多。默认是 alpha按路径字母排序也还不错看你个人习惯。2.2 通过 OpenAPI Bean 注入文档基础信息你肯定不希望打开的文档系统名称是默认的“OpenAPI 3”而是想写成你项目的实际名称。这就需要在启动类或者配置类里定义一个 OpenAPI BeanConfiguration public class OpenApiConfig { Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title(DEMO 项目接口文档) .description(这是 Spring Boot 3 springdoc 的示例接口文档) .version(v1.0.0) .contact(new Contact() .name(开发者团队) .email(devexample.com)) .license(new License() .name(Apache 2.0) .url(https://www.apache.org/licenses/LICENSE-2.0.html))) .externalDocs(new ExternalDocumentation() .description(项目文档中心) .url(https://docs.example.com)); } }这个 Bean 定义之后springdoc 会自动把它合并到 /v3/api-docs 返回的 JSON 里。所以你可以在文档里维护标题、版本、联系方式、协议信息这些这些信息对前端对接和测试同事来说非常有用。尤其是一套系统面向多个团队联调时文档里写清楚联系人邮箱能省掉很多沟通成本。2.3 按模块拆分组GroupedOpenApi 的典型用法当你的系统不止一个端比如有管理后台接口 API有用户端小程序 API还有内部定时任务的 API这时候全部混在一个文档里就特别难用。springdoc 提供了 GroupedOpenApi 来拆分组并且每个组可以独立配置路径匹配和包扫描。下面我以一个典型的多端项目为例Configuration public class SpringDocGroupConfig { Bean public GroupedOpenApi adminApi() { return GroupedOpenApi.builder() .group(admin) .pathsToMatch(/admin/**) .packagesToScan(com.example.demo.controller.admin) .build(); } Bean public GroupedOpenApi appApi() { return GroupedOpenApi.builder() .group(app) .pathsToMatch(/app/**) .packagesToScan(com.example.demo.controller.app) .addOpenApiCustomizer(openApi - openApi.info(new Info() .title(小程序端接口) .version(v1.0.0))) .build(); } }配置好之后Swagger UI 右上角就会出现一个下拉框你可以切换 admin 和 app 两组文档每组的接口列表是独立的。如果你的项目用到了 spring cloud gateway 这类聚合层还能通过配置把多个下游服务的文档聚合到一个 UI 里这个以后有机会再讲。现在我每次拿到一个多模块新项目都会先把分组这块写好后面加接口几乎不需要再动配置。3. 实操过程从零配置到内网可访问的 Swagger UI这里我直接把整个流程拆成步骤从创建项目到看到 UI 页面一步一步来。假设你本地已经有一个 Spring Boot 3.2.x 的 Maven 项目主类正常启动过。3.1 添加依赖并验证启动在 pom.xml 里添加依赖dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version2.6.0/version /dependency如果你用的是 Gradle就写成implementation org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0然后启动项目。启动成功后浏览器直接访问 http://localhost:8080/swagger-ui.html。正常情况下你会看到一个默认的 Swagger UI 页面左侧是接口列表中间是接口详情。如果没有页面而是 404那先访问一下 http://localhost:8080/v3/api-docs看看有没有 JSON 返回。这一步很关键因为页面和 JSON 是分开的两个端点。如果 JSON 正常但页面 404大概率是 swagger-ui 静态资源路径被某种拦截器拦了或者是项目里配置了全局的 Servlet Path。如果连 JSON 都 404那就是 springdoc 自身没生效或者你的 Servlet 上下文路径有问题。我遇到过一次诡异的情况Swagger UI 能打开但是接口列表是空白的只有页面框架后来发现是项目里配了 context-path: /demo正确地址变成了 http://localhost:8080/demo/swagger-ui.html我直接访问根路径当然拿不到页面。3.2 写一个示例接口看效果为了验证配置是否正常我习惯先写一个简单的 ControllerRestController RequestMapping(/api/hello) public class HelloController { Operation(summary 问候接口, description 传入名字返回问候语) GetMapping(/{name}) public ResultString sayHello(PathVariable(name) String name) { return Result.success(hello name); } }这里随手用了一个 Result 作为统一返回值实际上就是常见的 response 包装类。写完启动项目打开 Swagger UI找到 “GET /api/hello/{name}”点开之后你会看到参数说明里识别出了 name 这个路径变量响应体里会展示 Result 泛型结构。这个泛型的展开能力是 springdoc 做得比较好的地方它能通过解析 Type 信息把 Result 里的 data 字段推断成 string 类型。如果没有 Operation 注解接口也能显示只是描述会比较简陋。我个人的习惯是核心业务接口都加上 Operation给前端和测试人员一个清晰说明内部小接口或者临时接口就懒得写反正也能扫出来。3.3 常用注解到底该怎么加很多人对 Spring Fox 时代的那套 Api、ApiOperation 注解很熟悉但 springdoc 用的是 swagger 3 的注解也就是 Operation、Parameter、Schema、ApiResponse 这几个。它们在包名 io.swagger.v3.oas.annotations 下面和旧注解是完全不同的包。我在实际项目中优先给接口层加三个注解第一个是 Tag标注 Controller 的说明文字Tag(name 用户管理, description 用户相关的增删改查接口) RestController RequestMapping(/api/user) public class UserController { }第二个是 Operation标注每个接口的功能Operation(summary 创建用户, description 创建一个新用户需要传入用户名和邮箱) PostMapping(/create) public ResultLong createUser(RequestBody Valid UserCreateRequest request) { return Result.success(userService.create(request)); }第三个是 Schema用于标注请求模型和响应模型中的字段说明public class UserCreateRequest { Schema(description 用户名, example zhangsan, requiredMode Schema.RequiredMode.REQUIRED) NotBlank(message 用户名不能为空) private String username; Schema(description 邮箱, example zhangsanexample.com) Email private String email; }这里要特别注意Schema 的 requiredMode 只是文档展示层面的“必填”不是真正的校验逻辑真正起校验作用的是下面的 NotBlank、Email 这些 Jakarta Validation 注解。springdoc 在生成文档时会把两者合并所以文档里能看到这个字段是必填、长度限制、格式要求等但如果你不写校验注解文档里就不会显示那些约束。这其实也提醒一点如果你的项目里请求体没有使用 Jakarta Validationspringdoc 能展示的信息就会少很多建议尽量用规范的数据模型。4. 与 Spring Security 集成时最常见的权限处理方案现在几乎每个 Spring Boot 项目都会接入安全框架springdoc 的文档接口默认是没有任何认证的所以会被 Spring Security 拦截。你如果是自己调试可能直接被重定向到登录页或者返回 401。这里我给出两种常规解法。4.1 在 Security 配置里放行文档路径如果项目用的是 SecurityFilterChain 这种配置方式可以在 authorizeHttpRequests 里放行下面几个路径Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .requestMatchers( /swagger-ui.html, /swagger-ui/**, /v3/api-docs/**, /webjars/** ).permitAll() .anyRequest().authenticated() ); return http.build(); }注意这里的 /v3/api-docs/** 不要只写 /v3/api-docs因为 springdoc 默认会在这个路径下生成多个子路径比如 /v3/api-docs/swagger-config不放开的话 UI 页面虽然能打开但列表加载不出来看起来像是接口丢失。webjars 路径也要放行因为 swagger-ui 前端静态资源依赖 webjars。如果你还配置了 swagger-ui 的 oauth2-redirect 相关路径也要一起放行具体路径可以在日志里看到。我第一次配置的时候漏掉了 swagger-config 这个子路径结果页面一直转圈最后在浏览器 Network 里看到请求 /v3/api-docs/swagger-config 返回 401 才定位到问题。4.2 给调试请求自动带上 Bearer Token实际联调时我们的接口大部分需要 JWT 认证。你可以在 Swagger UI 的右上角点 Authorize 按钮手动输入 token但每次都手动粘贴太麻烦。更工程化的做法是在 springdoc 配置里声明一个全局的 Authorization 参数这样 swagger-ui 会显示一个全局的 Authorize 输入框输入一次 token后续所有请求都会自动带上。示例代码是这样的Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info().title(Api Documentation)) .components(new Components() .addSecuritySchemes(bearer-jwt, new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme(bearer) .bearerFormat(JWT) .in(SecurityScheme.In.HEADER) .name(Authorization))) .addSecurityItem(new SecurityRequirement().addList(bearer-jwt)); }配置之后Swagger UI 右上角会多一个 Authorize 按钮点击输入 token 值后续所有接口请求都会自动带上 Authorization: Bearer xxx。这个配置本身是 springdoc 基于 OpenAPI 规范的 SecurityScheme 实现的和写死请求头相比更规范。不过要注意这个全局 SecurityRequirement 会让所有接口都显示带锁图标并且默认都要求认证。如果有一些公开接口比如短信验证码、登录接口不想带锁可以在具体接口上用 SecurityRequirement(name ) 覆盖或者写自定义注解。这个我建议每个团队自己约定不要让所有接口都显示需要认证但实际不校验容易误导新同事。5. 常见问题与排查技巧实录springdoc 整体来说非常稳定但正因为它的自动装配能力很强一旦项目里出现了自定义拦截器、全局异常处理、路径前缀等配置就容易出现各种奇怪的问题。我把自己踩过和帮别人排查过的坑整理成表格大家可以对照来找问题。5.1 接口列表空白或只显示一部分原因之一是扫描路径问题。springdoc 默认扫描主类所在包及其子包如果你的 Controller 在另一个 Maven 模块里而且那个模块没有通过包扫描被发现那么接口就不会出现在文档里。这时候重点检查 packages-to-scan 有没有配置正确。另一个常见原因是路径匹配问题。如果你配置了 paths-to-match: /api/**但实际 Controller 的 RequestMapping 不是 /api 开头那自然就不显示。这个配置在多人协作项目里特别容易出问题因为每个人加的接口前缀不一样。还有一种隐蔽情况Spring Boot 全局配置了 servlet.path 或者 server.servlet.context-path这会导致 swagger-ui 的实际访问地址发生变化。你看到的根路径 404 可能只是路径不对并不代表文档没生成。用 curl 试一遍 /v3/api-docs 就能快速判断。5.2 泛型返回对象在文档里显示不友好大多数项目都会把返回结果封装成 Result 这种统一结构springdoc 对泛型的解析通常没问题但如果你用了很深的嵌套泛型比如 ResultPage 文档里可能会出现字段缺失或者泛型变量被解析成 object 而不是具体类型。我建议的解决思路是给返回类型写显式注解比如用 Schema(implementation Page.class)或者直接把接口返回类型定义成具体的 DTO 而不是带着一长串泛型的组合。在前后端对接阶段文档的清晰度比代码的简洁性更重要。如果你用了 Lombok 且 DTO 里的字段没有 getter/setterspringdoc 也可能扫不到字段因为它是基于 JavaBean 规范反射读取的没有 getter 的字段默认不会展示。5.3 引入 JSR-303 校验后报错springdoc 在生成参数描述时会读取 jakarta.validation 的注解如果项目里引入了 validation 依赖但缺少对应的可执行实现比如 hibernate-validator某些版本下启动时会出现 BeanValidation 相关的异常。解决办法很简单Spring Boot 项目直接引入 spring-boot-starter-validation 是最省事的方案。如果是纯 API 服务没有用校验可以不引入但那样接口文档的参数约束信息就少很多。5.4 Swagger UI 样式错乱或加载不出静态资源这通常和项目里的静态资源配置有关。如果你在 WebMvcConfigurer 里手动 override 了 addResourceHandlers又没有加上 springdoc 需要的那几个映射就会导致 swagger-ui 的 HTML 能出来但 JS/CSS 加载不出来。springdoc 默认会注册这些静态资源映射但如果你在自己代码里把默认映射覆盖了就要手动加回去。我在 Spring Boot 3.2 项目里遇到过一种情况用了 Spring Cloud Gateway 做网关下游服务的 swagger-ui 通过网关转发后页面空白排查下来是网关转发时没有保留 /v3/api-docs 的响应头前端拿到的是被包装过的 JSON 壳子。这类问题需要从前端网络请求链路去分析不要一上来就怀疑 springdoc 有问题。下面是问题速查表现象大概率原因解决方式访问 /swagger-ui.html 404context-path 或 Servlet path 不对访问 /context-path/swagger-ui.html页面能打开但接口列表空paths-to-match 配置导致 Controller 被过滤检查路径匹配规则放宽或去掉接口列表有但调用时 401Spring Security 拦截了接口请求在 Security 配置里放行文档路径/v3/api-docs 返回 JSON 但 UI 空白swagger-config 子路径被拦截放行 /v3/api-docs/** 和 /webjars/**泛型字段显示为 object返回类型不够显式或 DTO 缺 getter用具体 DTO 返回或加 Schema注解打开页面报 js 404静态资源映射被覆盖重新加回默认资源映射或检查网关转发5.5 多个 springdoc 版本依赖并存Spring Boot 3 自动装配会引入 springdoc 核心依赖如果你还手动引入了旧版的 springdoc-openapi-ui就可能出现类冲突。启动时通常会出现类似 NoClassDefFoundError 的异常。排查时先看 Maven 依赖树把旧版本排除掉。我建议依赖坐标统一查询 mvnrepository 网站不要凭记忆写版本号。6. 生产环境怎么处理关闭文档与控制访问范围很多团队把项目部署到测试环境或者生产环境后发现自己写的接口文档直接暴露出来了这其实是一个安全隐患。springdoc 默认是开启的所以上线前一定要处理好。6.1 按环境关闭 springdoc最简单粗暴的方式是在生产环境的 application-prod.yml 里设置springdoc: api-docs: enabled: false swagger-ui: enabled: false这样生产环境 /v3/api-docs 和 /swagger-ui.html 都会返回 404不会泄漏接口结构。但这个问题点是“全有或全无”如果测试环境也需要文档但某些内部管理接口不想让外部看到那就不能靠着这个全局开关解决。6.2 用 Security 控制文档访问权限更好的做法是文档接口本身可以打开但必须经过认证才能访问。在 Spring Security 配置里不放行文档路径而是要求登录后访问。这样内部测试人员输入账号密码后能看文档外部用户访问不了。如果你用的是 JWT 统一认证模式也可以把文档路径加入需要认证的白名单只有带合法 Token 才能访问。这个思路比直接关闭更灵活团队内部新同事入职后也不需要再让运维单独开端口才能看文档。6.3 自定义 swagger-ui 路径如果你不想用默认的 /swagger-ui.html可以通过配置修改springdoc: swagger-ui: path: /docs/api.html api-docs: path: /docs/api.json这样外部扫不到默认路径能起到一定程度上的“隐藏”效果但本质上还是不能作为安全方案。真正的安全控制还是靠认证和授权。7. 最后分享几个我在项目里的实操心得用 springdoc 也有近两年了从 Spring Boot 2.x 时代的 1.x 版本一路用到 3.x 时代的 2.x 版本整体体验比 springfox 好很多尤其是升级项目时的平滑度。这里分享几个我自己的总结性经验。第一点是版本号一定不要乱写。springdoc 1.x 对应 Spring Boot 2.xspringdoc 2.x 对应 Spring Boot 3.x这个对应关系很多教程里写得不清楚导致混搭后启动报错。我第一次升级 Spring Boot 3 的时候脑一热把旧项目里的 springdoc 从 1.6.9 升到了 1.7.0结果启动直接 NoClassDefFoundError排查半天才意识到要跳到 2.x 分支。第二点是配置分组要在项目初期就做好。如果等接口已经有几百个了再回头去拆分文档靠 paths-to-match 去匹配前缀那会特别痛苦。新项目我一般第一周就会把 GroupedOpenApi 配置写好然后让每个模块的 Controller 严格按 /api/admin、/api/app 这种前缀去组织后面加接口完全不用动配置。第三点是善用 Spring Boot 的 configuration properties 帮助文档。springdoc 的配置项非常多但你看它的 spring-configuration-metadata.json 文件就能找到所有参数。IDEA 里写 yml 时直接自动补全会比去网上搜博客快很多而且不容易被过时信息误导。第四点如果你团队的接口文档需要导出成离线 HTML可以试一下 springdoc 提供的 swagger-ui 页面里的 “生成文档” 功能或者用一些第三方工具把 /v3/api-docs 的 JSON 转成 markdown。遇到现场部署没网的情况提前导出离线文档就成了救命稻草。这个内容后续还可以扩展的方向包括把 springdoc 集成到 API 网关做统一文档入口、在 CI 阶段用 openapi-generator 自动生成客户端代码、用 springdoc 的扩展点把调试信息输出到日志文件里。我最近正在尝试把多服务文档聚合到一个独立项目中等跑完再单独写一篇分享。如果你在接入 springdoc 过程中遇到任何奇怪的报错欢迎在评论区把错误堆栈贴出来大家一起看看。