简介:围绕Spring Boot与OpenAPI 3文档化实践整理的示例工程合集,面向具备Java基础、需要为REST接口生成规范说明和交互式文档的后端开发者。压缩包共219个文件,含139个Java源码、23个XML配置、21个YAML文件及6个Markdown说明,整体约281KB,按示例模块组织,便于逐项查阅复用。已有967人学习。内容覆盖springdoc-openapi依赖接入、全局信息与接口注解、GroupedOpenApi分组、安全认证描述及Swagger UI调试等场景,可作为搭建API文档体系的参考模板,也适合对照示例排查注解使用、分组配置与文档展示中的常见问题,减少从零摸索成本。
1. 先搞懂一件事:接口文档为什么总是最先过期的那一份
刚进项目组的新手,大概率会遇到这样一幕:前端同学在群里问「这个用户列表接口,lastLoginTime 返回的到底是时间戳还是字符串」,后端同学翻了两分钟代码,回一句「等等我看下」。这种对话天天发生,根源在于文档和代码是两份东西,代码一改,文档立刻变成历史遗迹,指望人手动同步基本等于玄学。OpenAPI 3 就是给接口写说明书的一套标准格式,而 springdoc-openapi 是那个照着你代码自动把说明书填好的工人,配合 Spring Boot 用,几乎不用额外付出。这份 springdoc-openapi-demos 演示仓库,做的就是把这些工人干活的场景,一个一个摆出来给你看:一个最小的接口长什么样,加了注解之后页面怎么变,不同用法之间有什么区别。如果你刚学 Java 后端,写过 Controller 但从没认真管过接口文档,那这篇入门就是写给你的。
2. 把两个名词说人话:OpenAPI 3 是说明书格式,springdoc 是写说明书的工人
2.1 OpenAPI 3 到底是什么:一份给机器看也给人看的菜单
把接口文档想象成餐厅菜单。菜单上每道菜要写清楚:叫什么名字、多少钱、有没有辣、要不要提前预订。接口也一样,一个「查询用户列表」的接口,得说清楚:请求方法是什么、路径是什么、要不要传参数、返回的 JSON 里每个字段是什么类型、出错时返回什么。OpenAPI 就是规定「这份菜单该怎么排版」的标准,只不过它规定的排版格式是 YAML 或者 JSON,机器能读,人也能读。
一份最小的 OpenAPI 3 描述文件大概长这样:
# OpenAPI 3 描述文件的最小骨架 openapi: 3.0.1 # 规范版本,3.x 和 2.0 是两套写法,不能混 info: title: 用户服务接口 # 这份文档叫什么,会显示在页面顶部 version: 1.0.0 # 接口版本,不是代码版本 paths: # 所有接口路径都挂在 paths 下面 /api/users: get: summary: 查询用户列表 # 一句话说明这个接口干什么 responses: '200': description: 成功 # 返回值的描述,真正返回结构在 content 里写看到这里你可能会想:这不就是一份手写的 JSON 吗,跟我有什么关系?关键就在于,这份文件本来是应该由人一个字一个字敲出来的,接口有二十个、字段有上百个的时候,手敲就是一场灾难。而且它最大的问题是会过期 —— 代码改了,这份 YAML 没人改,前端照着它联调,直接翻车。
所以 OpenAPI 的价值不在于「多了一种文档格式」,而在于它是一份机器可以自动生成的格式。只要有个工具能读懂你的 Java 代码,把代码里的路径、参数、返回值翻译成上面这种结构,文档就永远不会和代码脱节。这个工具,就是 springdoc-openapi。
顺带提一句版本的事。你可能在老项目里见过 Swagger 2.0 这个词,它是 OpenAPI 的前身,格式和 OpenAPI 3 不一样,注解包名也不一样。新手最容易犯的错就是把两套东西的依赖和注解混着用,这个问题后面避坑那章会专门讲。
2.2 springdoc-openapi 干的活:读你的代码,反推出一份说明书
springdoc-openapi 的定位,说白了就是一个翻译官。你的 Spring Boot 项目启动的时候,它会把项目里所有带@RestController、@GetMapping这类注解的类扫描一遍,从方法签名里推断出:这是个 GET 请求、路径是/api/users、返回值是List<User>、User里面有 id、name、email 三个字段。推断完,它把这些信息拼成 2.1 里那种 JSON,挂在一个固定地址上,默认是/v3/api-docs。
这个地址返回的是纯 JSON,人看着累。所以 springdoc 还顺手带了一个网页界面,叫 Swagger UI,地址通常是/swagger-ui/index.html。这个页面会把刚才那份 JSON 渲染成可以展开、可以点「Try it out」直接发请求的可视化文档。你什么都不用写,加个依赖、启动项目,页面就已经有了,接口列表、参数、返回结构全在里面。
我在第一次接触的时候,最大的感受是「黑匣子感」消失了。以前接口文档是一个独立于代码之外的东西,谁写的、什么时候写的、准不准,全靠自觉。现在文档就是代码的投影,代码是唯一真相,文档是它照出来的一面镜子。镜子会不会照歪?会,比如你的返回值用了泛型嵌套、或者用了Map<String, Object>这种说不清结构的类型,它就只能照出一个大概。但对于日常增删改查,准确率已经足够高了。
这里要区分清楚两件事:springdoc-openapi 是库,你把它加进自己的项目里,它帮你生成文档;springdoc-openapi-demos 是演示仓库,里面是一个个已经配好的小工程,用来演示这个库在不同场景下怎么用。新手常见的误解是把演示仓库当成库本身,clone 下来一通改,结果不知道自己改的是示例还是框架。
2.3 演示仓库怎么读:先找最小的那个,别一上来就全量 clone
打开这个演示仓库,第一反应可能是「这么多工程,我该看哪个」。给一个新手最省力的读法:
第一步,不要 clone 整个仓库。仓库里每个演示工程都是独立的,你只需要关心那个依赖最少、启动类最干净、只写了一两个接口的模块。判断标准很简单:打开它的pom.xml,如果里面只有 Spring Boot 的 web 依赖加上 springdoc 的依赖,没有安全框架、没有数据库、没有消息队列,那就是它。
第二步,把它单独下载下来,先用mvn spring-boot:run跑起来,什么都别改,先确认能在浏览器里看到那个页面。这一步的意义是建立一个「正常长什么样」的基线,后面你自己写代码出问题时,才知道是哪里不一样了。
第三步,再回头去看它和你的项目差在哪。差在依赖?差在注解?差在配置?演示工程的价值不是让你抄它的代码,而是给你一个参照物,让你在自己项目出问题时能快速做对比。
提示:读演示仓库的顺序建议是「能跑起来的最小示例 → 加了注解的示例 → 带安全认证或分组的示例」,跳着看容易一头雾水。
3. 为什么值得学:三个真实到扎心的联调场景
3.1 场景一:改了一个字段名,文档没人改,前端白干半天
假设你负责一个用户接口,某天产品说要和国际规范对齐,把返回字段userName改成username。你改完代码,本地测试通过,提交上线。三天后前端找过来,说他那边一直取不到用户名,页面上一片空白。
问题出在哪?前端是照着两周前那份手写的接口文档写的,那份文档里还写着userName。文档是一份独立的 Word 或者 Markdown 文件,躺在某个共享目录里,没有任何机制提醒任何人去改它。这种事发生一次,损失的可能是半天工时;发生十次,团队里就没人信文档了,所有人改成「有问题直接问后端」。
springdoc 改变的就是这个链条。它生成的文档不是独立文件,是每次启动都从代码重新推导出来的。你把字段名改了,重启项目,页面上的字段名跟着变。前端只要打开那个地址,看到的就是当下这一刻的真实情况,不存在「文档过期」这个概念。
3.2 场景二:前端只能靠猜,或者靠翻你的代码
第二种情况更常见:项目压根没文档。前端要对接一个新接口,只能去翻后端代码,或者干脆在群里问「这个参数必填吗」「传空字符串行不行」「分页是从 0 开始还是从 1 开始」。
面试的时候很多人会说「我写过接口文档」,实际工作中大部分团队的「文档」就是一句口头约定。这不能怪谁懒,手写文档这事本身就反人性:写完没人看,改代码时又想不起来改它。
用 springdoc 之后,基础信息是自动的:路径、方法、参数名、参数类型、是否必填(取决于你有没有用@RequestParam(required = ...))、返回结构,这些不需要你额外写一行。你唯一需要补的,是那些代码表达不出来的业务含义,比如「这个 status 字段,0 是正常、1 是冻结、2 是注销」。这类信息用@Schema或者@Operation补一句描述就行,工作量从「写一整份文档」降到「给关键字段加一句话」。
3.3 场景三:文档和代码是两个真相,测试和运维都被坑
第三种情况最隐蔽。接口文档看起来挺全,但它记录的是三个月前的状态。新来的测试同学照着它设计用例,测出来一堆「不符合预期」,开发一看说「文档没更新,现在不是这样了」。运维排查线上问题,想确认某个接口的返回结构,看了一眼文档,被误导得更深。
这背后的本质问题是:当一份信息有两个来源,就一定会分叉。代码是一个来源,文档是另一个来源,只要它们不是同源的,早晚会不一致。springdoc 的思路是把文档变成代码的派生物,让来源收敛成一个。这听起来是个小改动,实际对协作效率的影响很大 —— 新人接手项目第一件事不是读代码,而是打开那个页面点一遍,十分钟就能把接口全貌摸清楚。
3.4 顺带说说 springfox:为什么新项目基本不选它了
如果你搜资料,一定会看到另一个名字:springfox。它是上一代做同样事情的库,很多老项目还在用。新手容易困惑:这俩到底啥区别,我该学哪个?
简单的判断是:新项目直接用 springdoc,老项目才需要考虑 springfox。原因是 springfox 对 Spring Boot 2.6 之后的版本兼容性出了名地差,经常出现启动报错、页面 404 这类问题,社区维护也慢下来了。springdoc 是后来者,专门针对新版 Spring Boot 设计,支持 OpenAPI 3 规范,配置也更简单。
但这不代表你可以无视 springfox 的存在。真实情况是,很多公司项目还在用老版本,你接手的时候会遇到一堆io.swagger.annotations.ApiOperation这种注解,而 springdoc 用的是io.swagger.v3.oas.annotations.Operation。两套注解包名相似但完全不同,混用会编译不过或者注解不生效。这个坑在第 5 章会细讲。
4. 第一个例子跑通:五步从空工程到能点的接口页面
4.1 第一步:准备环境,建一个干净的 Spring Boot 工程
前置条件只有两个:装好 JDK(Spring Boot 3 需要 JDK 17 及以上,Spring Boot 2 用 JDK 8 也行),装好 Maven 或者 Gradle。版本怎么看?命令行敲一下:
# 查看 JDK 版本,确认是 17 或以上 java -version # 查看 Maven 版本,能跑就行 mvn -v然后建工程。最省事的方式是在 IDE 里用 Spring Boot 工程向导,或者在官方脚手架页面填几项:构建工具选 Maven,语言 Java,Spring Boot 版本选一个稳定版,依赖只勾 Spring Web。生成后解压,用 IDE 打开,确认能启动。
这一步有个新手常犯的错:勾了一堆依赖,什么 Lombok、JPA、Security 全勾上。结果第一次启动就报错,然后就卡在「到底哪里出问题了」。第一次跑通,越干净越好,只留 Spring Web,其他的等文档页面出来了再加。
4.2 第二步:加依赖,注意两个 artifactId 的区别
打开pom.xml,加 springdoc 的依赖。这里有一个分岔口,选错了就直接 404:
<!-- 在 properties 里声明版本,方便统一管理 --> <properties> <springdoc.version>2.5.0</springdoc.version> </properties> <dependencies> <!-- Spring Boot 3.x 用这个:注意 artifactId 里带 starter --> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>${springdoc.version}</version> </dependency> <!-- 如果你是 Spring Boot 2.x,要换成下面这个: <artifactId>springdoc-openapi-ui</artifactId> 并且版本号要用 1.x 系列,不能填 2.x --> </dependencies>这三个参数各自是什么意思,值得说清楚:
groupId固定是org.springdoc,这是项目组织标识,不会有第二种写法。artifactId是关键,springdoc-openapi-starter-webmvc-ui这个名字里,starter表示它是给 Spring Boot 3 用的(Boot 3 之后官方推荐 starter 命名),webmvc表示你的项目是传统的 Servlet 栈(如果你用 WebFlux 响应式栈,要换成带webflux的那一个),ui表示它同时带上了 Swagger UI 那个网页界面 —— 如果只想要 JSON 不要页面,去掉-ui就行。version一定要和 Spring Boot 大版本对应:Boot 3 配 2.x,Boot 2 配 1.x,跨版本基本必炸。
版本号我上面写了一个具体的数字作为示例,实际用的时候建议去仓库页面看一眼当前最新的稳定版,不用死记。
4.3 第三步:写一个最小的接口,先不接数据库
建一个UserController,硬编码两条数据返回,目的是把注意力全放在文档上,不要被数据库配置分散:
package com.example.demo; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController // 这个注解是必须的,springdoc 靠它识别出这是接口类 @RequestMapping("/api/users") // 统一路径前缀,下面方法的路径会拼在它后面 public class UserController { // 内存里造两条假数据,先不接数据库 private final List<User> users = List.of( new User(1L, "A同学", "a@example.com"), new User(2L, "B同学", "b@example.com") ); @GetMapping // 对应 GET /api/users public List<User> list() { return users; } }再补一个User类型。这里用 Java 16 之后支持的 record,一行就写完了:
package com.example.demo; // record 会自动生成构造方法、getter 和 equals,字段结构一目了然 public record User(Long id, String name, String email) { }这段代码的逻辑很直白:@RestController告诉 Spring 这个类里的方法返回值直接作为响应体,@RequestMapping定了一个前缀路径,@GetMapping表示这个方法响应 GET 请求。springdoc 在启动时读到的信息就是:有一个路径/api/users,方法 GET,返回一个数组,数组元素有三个字段,类型分别是数字、字符串、字符串。它不需要你额外告诉它这些,全是从代码里推出来的。
4.4 第四步:启动项目,访问两个地址
用 IDE 的运行按钮,或者命令行:
# 在项目根目录执行,启动 Spring Boot mvn spring-boot:run看到日志里出现Tomcat started on port(s): 8080就说明起来了。然后打开浏览器,试这两个地址:
第一个,http://localhost:8080/v3/api-docs。你会看到一大坨 JSON,别被吓到,这就是 2.1 里说的那份「机器可读的说明书」。往下翻,能找到paths下面挂着/api/users,还有components里对User结构的描述。这份 JSON 才是真正的产物,页面只是它的可视化。
第二个,http://localhost:8080/swagger-ui/index.html。这才是给人看的。页面上应该能看到一个分组,里面列着GET /api/users,点开可以展开看到返回结构,右侧有个「Try it out」按钮,点一下再点「Execute」,能直接发出请求并看到真实返回。
注意:老资料里常说访问
/swagger-ui.html,新版里这个地址会自动跳转到/swagger-ui/index.html。如果你访问前者报 404,直接试后者,别急着怀疑依赖没加对。
到这里,第一个例子就跑通了。整个过程你没写一行文档,页面已经出来了。这就是这个库最核心的价值 —— 先让你尝到甜头,再谈怎么把它调得更准。
4.5 第五步:加第一个注解,让页面从「能用」变成「能看懂」
自动生成的内容有个短板:全是技术信息,没有业务含义。比如email字段,页面上只写string,不写它是登录邮箱还是备用邮箱。这一步就要靠注解补。
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @Tag(name = "用户管理", description = "用户的查询相关接口") // 给整个类打个标签,页面上会分组显示 @RestController @RequestMapping("/api/users") public class UserController { @Operation( summary = "查询用户列表", // 显示在接口名旁边的短标题 description = "返回内存中的全部用户,仅用于演示,不接数据库" // 展开后的详细说明 ) @GetMapping public List<User> list() { return users; } }注解写完之后,必须重启项目才生效 —— springdoc 是在启动阶段扫描注解的,热更新有时候不重载这部分元数据,改完看不到变化先重启再说。
两个注解的作用分别是:@Tag作用在类上,把同一类的接口归到一个分组,页面上显示成「用户管理」;@Operation作用在方法上,给单个接口补标题和描述。这两个是最常用的,先把它们用熟,其余的@Parameter(描述单个参数)、@Schema(描述数据模型里的字段)、@ApiResponse(描述各种返回状态码)可以后面按需加。
字段级的描述可以这么加:
import io.swagger.v3.oas.annotations.media.Schema; public record User( @Schema(description = "用户唯一标识", example = "1") Long id, @Schema(description = "显示名称", example = "A同学") String name, @Schema(description = "登录邮箱", example = "a@example.com") String email ) { }description会显示在字段说明里,example会填进「Try it out」的示例请求中,前端拿这个示例一粘贴就能测。别小看这个 example,联调时它能省掉大量「这个字段该传什么格式」的来回确认。
5. 新手避坑清单:五个最容易翻车的地方和它们的解法
5.1 页面打不开,浏览器一片 404
现象:项目启动没报错,日志看着也正常,但访问/swagger-ui/index.html就是 404,或者访问/v3/api-docs返回一段错误 JSON。
原因:最常见的是依赖选错了。Spring Boot 3 的项目里加了 1.x 版本的springdoc-openapi-ui,包的命名空间还是老的javax,跟 Boot 3 用的jakarta对不上,组件根本没注册进去。第二种可能是项目配置了统一的路径前缀,比如server.servlet.context-path: /app,那所有地址前面都得加上这个前缀,你访问/swagger-ui/index.html自然是找不到的。
解决:先看pom.xml里 artifactId 和版本对不对,Boot 3 必须是带starter的那个加 2.x 版本。再检查application.yml里有没有配context-path,有的话访问地址要变成/app/swagger-ui/index.html。最后确认启动类的包路径能扫描到你的 Controller,扫描不到的话连接口都不会有,但页面本身还是能打开的 —— 如果页面都打不开,问题一定在依赖或路径上。
5.2 页面能打开,但接口列表是空的
现象:Swagger UI 页面正常渲染,标题也在,就是下面一个接口都没有,显示「No operations defined in spec」。
原因:三种可能。第一种,Controller 类不在启动类所在的包或者子包下,Spring 根本没扫描到它,自然也不会被 springdoc 读到。第二种,你用的是@Controller而不是@RestController,或者方法上忘了加@ResponseBody,springdoc 认为这不是一个返回 JSON 的接口。第三种,项目里引入了安全框架,/v3/api-docs这个地址被拦截了,springdoc 拿不到数据源,页面就空了。
解决:先确认包结构,启动类在最外层,Controller 在它下面的子包里。再检查类上的注解。如果用了安全框架,需要在配置里放行/v3/api-docs/**、/swagger-ui/**、/swagger-ui.html这几个路径。这一步新手特别容易漏,因为页面能打开会让你误以为没被拦截,实际上页面是静态资源,JSON 接口才是被拦的那个。
5.3 启动直接报错,一堆 ClassNotFound 或者 NoSuchMethod
现象:加完依赖,项目起不来了,日志里一片红,报ClassNotFoundException: javax.servlet.http.HttpServletRequest或者类似的方法找不到。
原因:这是典型的版本冲突。项目里同时存在 springfox 和 springdoc 两套库,或者 Boot 3 项目里混进了基于javax命名空间的老包。Spring Boot 3 把整个生态从javax.*迁到了jakarta.*,任何还依赖老命名空间的库都会在启动时炸掉。
解决:全局搜一下pom.xml和依赖树里有没有 springfox 相关的包,有就排除掉。用mvn dependency:tree看一眼依赖树,找到冲突来源。另外注意,如果你是从老项目复制代码过来的,代码里 import 的io.swagger.annotations.*也要全部换成io.swagger.v3.oas.annotations.*,这两套注解长得像但不通用。
5.4 注解明明加了,页面上却一点变化都没有
现象:给方法加了@Operation(summary = "..."),重启之后页面上还是原来的默认标题,描述也没出现。
原因:头号原因是 import 错了包。IDE 的自动补全很容易帮你导进 springfox 那套老注解,编译不报错,但 springdoc 不认。第二个原因是注解加的位置不对 —— 如果你把接口定义在 interface 上、实现放在 impl 类里,注解要加在 interface 的方法上,加在实现类上会被忽略。第三个原因是改完没重启,或者用了热部署工具但注解元数据没重新加载。
解决:点开 import 那一行看一眼包名,必须是io.swagger.v3.oas.annotations。位置问题就统一约定:注解写在对外暴露的那一层。改完老老实实停掉重启,别指望热更新。这个坑我自己踩过不止一次,每次都是「明明写了啊」,最后发现是导错包。
5.5 把它当成「写完代码就有了」,忽略了它读的是代码本身
现象:接口返回的是一个Map<String, Object>或者一个泛型套泛型的结构,页面上显示的是{},什么字段都没有。又或者返回结构里有一堆内部字段,全暴露在文档上了。
原因:springdoc 是反推,不是理解。它能推出来的前提是你的代码本身把结构写清楚了。Map<String, Object>这种类型在编译后没有任何字段信息,它只能给你一个空对象。反过来,如果你直接把数据库实体类当返回值,所有字段包括内部标记位都会出现在文档上。
解决:返回值尽量用明确的类型,别用 Map 糊弄。对外暴露的接口单独定义一套返回对象,别直接复用数据库实体。如果确实有动态结构,用@Schema手动描述,或者在配置里做过滤。这一点算是血泪经验:自动生成不等于自动正确,你喂给它的代码越规范,它输出得越准。
6. 下一步学什么:从「能打开页面」到「能交付一份接口契约」
跑通第一个例子之后,很多人会停在「页面能打开就行」这个阶段。这其实只用了这个库两成的能力。往下走,建议按这个顺序补:
| 阶段 | 目标 | 重点看什么 |
|---|---|---|
| 第一周 | 注解用熟 | @Tag、@Operation、@Parameter、@Schema、@ApiResponse这五个覆盖日常九成场景 |
| 第二周 | 分组与配置 | 按业务模块把接口拆成多个分组,配置页面标题、版本、排序 |
| 第三周 | 与安全框架共存 | 放行文档路径,同时让文档能带上认证信息去调试需要登录的接口 |
| 第四周 | 契约意识 | 理解这份 JSON 不只是给人看的,还能被前端用来生成请求代码、被测试用来生成用例 |
第一周的重点是把注解从「会写」变成「写得准」。具体做法是,挑一个你手头真实的接口,把它的每个参数、每种返回状态都补上描述,然后打开页面,以前端的视角读一遍 —— 如果你读完还是不知道某个字段该传什么,说明描述没写到位。
第二周开始接触分组。默认情况下所有接口挤在一个页面里,项目稍大就找不着北。分组的方式是按业务模块拆,比如用户、订单、支付各一组,每组有自己的路径前缀,配置起来不复杂,但对可用性的提升很明显。
第三周会碰到认证的问题。这时候你会发现页面上的「Try it out」发出去的请求是没带 token 的,需要登录的接口一律返回未授权。解决方式是在配置里声明认证方式,让页面右上角出现一个可以填 token 的按钮。这一步是很多教程跳过的地方,但真实项目里绕不开。
第四周是观念上的转变。这份自动生成的 JSON,本质是一份接口契约:前端可以拿它生成请求代码,测试可以拿它生成用例,甚至可以拿它做接口变更的比对,看看这次上线改了哪些字段。到了这一步,你就不再是「加了个文档插件」,而是在用契约的方式管理前后端协作。
我自己的习惯是,每次提测前打开那个页面,从头到尾点一遍所有接口,确认参数说明、返回示例、错误码都是最新的。这个动作花不了十分钟,但能挡掉大部分「联调时才发现字段对不上」的返工。这个习惯保持下来之后,我再也没在群里被前端追着问过字段类型。希望帮到你。
本文还有配套的精品资源,点击获取