☰
OpenAPI与Swagger实战:从代码生成到CI质量门禁
2026/10/10 10:37:32 网站建设 项目流程

1. 从一份被投诉的接口文档说起

去年年底,团队里负责对接外部合作方的小组收到了一封措辞相当不客气的邮件。对方的技术负责人列了整整两页问题:字段类型标注不一致、错误码没有统一说明、分页参数在三个接口里出现了三种写法、示例请求体里还残留着测试环境的域名。最要命的是,这份文档是我们手动维护在一个内部协作平台上的,更新滞后了将近两周,而这两周里后端已经改了四个接口的返回结构。

这件事之后,我们下定决心把API文档的生成方式彻底翻新。核心思路只有一条:让文档从代码里长出来,而不是靠人去手写维护。围绕这个目标,我们最终落地了一套基于OpenAPI规范、用Swagger系工具链做呈现和调试的方案。这套东西说起来概念不复杂,但真正在项目里跑通、跑顺,中间踩的坑一点都不少。

这篇内容适合三类人看:一是正在被手写文档折磨、想找一套可持续方案的后端开发者;二是需要和前端、测试、外部合作方频繁对接接口的团队负责人;三是刚接触OpenAPI和Swagger、分不清这俩到底啥关系的初学者。我会从概念辨析讲起,一直讲到注解怎么写、UI怎么配、CI里怎么卡质量,把整套流程里那些文档上不会写的经验都摊开来说。

先给一个最直白的结论:OpenAPI是规范,Swagger是实现这套规范的一堆工具。很多人把这两个词混着用,导致沟通时经常鸡同鸭讲。搞清楚这个区别,后面的所有选择都会顺理成章。

2. OpenAPI与Swagger到底谁是谁

2.1 一个规范,一套工具链

OpenAPI Specification(简称OAS)是一份语言无关的接口描述规范,它定义了一个JSON或YAML文件应该长什么样,才能完整描述一组HTTP接口:路径、方法、参数、请求体、响应体、状态码、鉴权方式等等。你可以把它理解成"接口的身份证模板"——只要按这个模板填,任何懂这套规范的工具都能读懂你的接口。

Swagger则是一整套围绕OpenAPI规范构建的工具集合。最早Swagger规范本身就是OpenAPI的前身,后来规范捐给了Linux基金会下的OpenAPI Initiative,改名OpenAPI,而Swagger这个名字保留下来专指工具链。所以现在你听到的Swagger,通常指的是这几样东西:

  • Swagger Editor:在线或本地的编辑器,左边写YAML/JSON,右边实时预览文档。
  • Swagger UI:把OpenAPI描述文件渲染成可交互网页的工具,能直接在页面上发请求。
  • Swagger Codegen:根据描述文件生成客户端SDK或服务端桩代码。
  • Swagger Hub:托管和协作平台(商业产品)。

在Java生态里,还有两个高频出现的库需要区分清楚:springfox和springdoc。前者是老牌选手,支持Swagger 2规范,对Spring Boot 2.6以上版本兼容性越来越差;后者是后起之秀,直接支持OpenAPI 3规范,和Spring Boot新版本配合得更好。我们项目在选型时就是因为springfox在新版本Spring Boot上各种报错,果断换成了springdoc。

2.2 为什么非要选OpenAPI 3而不是Swagger 2

这个问题在选型会上被反复问过。Swagger 2的生态确实成熟,很多老项目还在用,但OpenAPI 3有几个实打实的优势让我们无法拒绝:

对比维度Swagger 2OpenAPI 3
请求体描述用body参数,和form参数混在一起独立的requestBody对象,支持多content-type
响应描述只能按状态码描述支持按content-type区分不同响应结构
组件复用definitions和parameters分开统一到components下,支持更多类型
示例支持较弱支持example、examples多示例
回调与链接不支持支持callbacks和links

最直观的差别在多content-type支持上。我们有个上传接口,既接受application/json的元数据,又接受multipart/form-data的文件流,Swagger 2描述起来非常别扭,OpenAPI 3用requestBody下的content字段就能干净地表达。所以除非有历史包袱,新项目一律上OpenAPI 3。

2.3 描述文件长什么样

在动手写注解之前,先看一眼原生的OpenAPI描述文件,建立直观印象。下面是一个精简的例子:

openapi: 3.0.3 info: title: 订单服务接口 version: 1.2.0 description: 提供订单创建、查询、取消能力 paths: /orders/{orderId}: get: summary: 查询订单详情 parameters: - name: orderId in: path required: true schema: type: string responses: '200': description: 查询成功 content: application/json: schema: $ref: '#/components/schemas/Order' '404': description: 订单不存在 components: schemas: Order: type: object properties: id: type: string amount: type: number format: double status: type: string enum: [CREATED, PAID, CANCELLED]

这份文件就是整个方案的核心资产。Swagger UI读它、Codegen读它、自动化测试工具也读它。理解了它的结构,后面用注解生成它,就只是"把代码翻译成这份文件"的过程。

3. 在Spring Boot项目里落地springdoc

3.1 依赖引入与版本匹配的坑

我们用的是Spring Boot 3.x,对应的springdoc版本是2.x。这里有个非常容易踩的坑:springdoc 1.x对应Spring Boot 2.x,springdoc 2.x对应Spring Boot 3.x,版本选错会直接启动失败,报一堆javax和jakarta包冲突的错。因为Spring Boot 3把javax.全面换成了jakarta.,而springdoc 1.x还在用javax。

Maven依赖这样写:

<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.3.0</version> </dependency>

如果你用的是WebFlux而不是WebMvc,把artifactId换成springdoc-openapi-starter-webflux-ui。这个细节很多人第一次会忽略,结果UI页面死活打不开。

引入之后,默认访问路径是/swagger-ui.html,它会自动重定向到/swagger-ui/index.html。描述文件的默认地址是/v3/api-docs。这两个地址建议记牢,后面配置和排查都要用。

3.2 全局配置:别让默认值坑了你

springdoc的默认配置能跑,但生产环境直接暴露会有问题。我们在application.yml里做了这些调整:

springdoc: api-docs: path: /v3/api-docs enabled: true swagger-ui: path: /swagger-ui.html tags-sorter: alpha operations-sorter: method disable-swagger-default-url: true packages-to-scan: com.example.order.controller paths-to-match: /api/**

几个关键点解释一下。packages-to-scan限定扫描范围,避免把一些内部管理接口也扫进去;paths-to-match只匹配/api/**开头的路径,把actuator那些监控端点排除掉;tags-sorter和operations-sorter让UI里的接口按字母和HTTP方法排序,接口多了以后找起来方便很多。

注意:生产环境一定要通过配置或网关把swagger-ui和api-docs的访问关掉或限制内网访问。我们见过有团队把带完整接口信息的文档页直接暴露在公网,等于把系统结构图送给了别人。

3.3 用注解把接口信息写进代码

springdoc的核心注解来自io.swagger.v3.oas.annotations包。最常用的几个是@Tag、@Operation、@Parameter、@Schema。看一个完整的Controller示例:

@RestController @RequestMapping("/api/orders") @Tag(name = "订单管理", description = "订单的创建、查询与取消") public class OrderController { @Operation(summary = "创建订单", description = "根据商品和数量创建一笔新订单") @ApiResponses({ @ApiResponse(responseCode = "200", description = "创建成功"), @ApiResponse(responseCode = "400", description = "参数校验失败"), @ApiResponse(responseCode = "409", description = "库存不足") }) @PostMapping public Result<OrderVO> create( @RequestBody @Valid CreateOrderDTO dto) { return Result.ok(orderService.create(dto)); } @Operation(summary = "查询订单详情") @GetMapping("/{orderId}") public Result<OrderVO> detail( @Parameter(description = "订单ID", required = true) @PathVariable String orderId) { return Result.ok(orderService.detail(orderId)); } }

DTO上的字段描述用@Schema:

public class CreateOrderDTO { @Schema(description = "商品ID", example = "SKU10086", requiredMode = RequiredMode.REQUIRED) private String skuId; @Schema(description = "购买数量", example = "2", minimum = "1", maximum = "99") private Integer quantity; }

这里有个经验:example的值一定要填真实可用的样例。我们早期偷懒填了"string"、"0"这种占位符,结果前端同学直接复制到调试工具里发请求,全部报参数错误,反过来投诉文档没用。后来统一要求example必须是能跑通的真实值,这个问题就消失了。

4. 让文档质量在CI里被卡住

4.1 为什么文档也需要质量门禁

文档写得好不好,靠人自觉是靠不住的。项目一忙,注解就懒得更新,几个月后文档和代码又对不上了。我们的做法是把文档质量检查塞进CI流水线,让它在合并请求阶段就暴露问题。

具体检查三类东西:一是描述文件能否正常生成(生成失败说明注解有语法问题);二是所有接口是否都有summary和description;三是所有DTO字段是否都有description。后两项用脚本扫描生成的OpenAPI JSON就能实现。

4.2 用脚本扫描缺失的描述

思路很简单:在CI里先启动应用或直接调用生成接口拿到/v3/api-docs的JSON,然后用一段Python脚本遍历,找出缺字段的地方。

import json import sys with open("openapi.json", "r", encoding="utf-8") as f: spec = json.load(f) missing = [] for path, methods in spec.get("paths", {}).items(): for method, detail in methods.items(): if method not in ("get", "post", "put", "delete", "patch"): continue if not detail.get("summary"): missing.append(f"{method.upper()} {path} 缺少 summary") if not detail.get("description"): missing.append(f"{method.upper()} {path} 缺少 description") if missing: print("文档质量检查未通过:") for item in missing: print(" -", item) sys.exit(1) print("文档质量检查通过")

这段脚本挂到CI的某个阶段,不通过就阻断合并。刚开始团队会有点抵触,觉得增加了负担,但两周之后就习惯了,因为写注解本来就是顺手的事,被卡一次比被合作方投诉十次划算得多。

4.3 把描述文件作为构建产物归档

每次构建时,把生成的openapi.json作为产物归档,好处有两个。一是可以追溯历史版本,接口什么时候改的、改成什么样,翻归档文件一目了然。二是可以拿它做契约测试,前端可以基于某个版本的描述文件生成mock服务,后端没写完也能先联调。

我们用的是在构建脚本里加一步curl:

curl -s http://localhost:8080/v3/api-docs -o openapi.json

然后在流水线的产物配置里把这个文件声明为归档项。这一步几乎零成本,但收益很大。

5. 那些文档里不会写的踩坑记录

5.1 泛型返回类型被吞掉的问题

我们统一用Result<T>包装返回,结果发现Swagger UI里所有接口的响应schema都显示成Result,里面的泛型T完全丢失,看不到具体字段。这是Java泛型擦除导致的经典问题。

解决办法是在方法上显式指定响应类型,用@Operation配合@ApiResponse的content,或者更简单地,在@Schema里用implementation指定。springdoc对泛型的支持其实做了不少工作,但遇到多层嵌套泛型时还是会力不从心。我们的做法是给每个具体返回类型定义一个别名类,比如Result<OrderVO>就定义一个OrderResult extends Result<OrderVO>,虽然有点笨,但UI里显示得清清楚楚。

5.2 日期格式在文档和实际返回里不一致

DTO里有个LocalDateTime字段,文档里显示成string,但没说明格式。前端按ISO格式解析,结果后端配置的Jackson序列化格式是yyyy-MM-dd HH:mm:ss,两边对不上,联调时排查了半天。

后来我们在@Schema里显式标注格式:

@Schema(description = "创建时间", example = "2024-01-15 10:30:00", type = "string", format = "date-time") private LocalDateTime createTime;

同时在全局配置里统一Jackson的日期格式,让文档、实际返回、前端解析三者对齐。这个坑的教训是:凡是格式敏感的类型,都要在文档里写死格式并给真实示例,不能指望别人去猜。

5.3 分组配置让接口不再一锅粥

项目大了以后,所有接口堆在一个页面里,找起来非常痛苦。springdoc支持用GroupedOpenApi做分组:

@Bean public GroupedOpenApi orderApi() { return GroupedOpenApi.builder() .group("订单服务") .pathsToMatch("/api/orders/**") .build(); } @Bean public GroupedOpenApi userApi() { return GroupedOpenApi.builder() .group("用户服务") .pathsToMatch("/api/users/**") .build(); }

配置之后,Swagger UI右上角会出现分组下拉框,可以按业务域切换。我们按业务域分了六组,对接方只需要看自己关心的那组,清爽很多。

5.4 鉴权信息怎么在UI里带上

内部接口需要登录态,Swagger UI默认发请求不带token,导致所有需要鉴权的接口都返回401,没法在线调试。解决办法是在配置里声明安全方案:

@Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info().title("订单服务").version("1.0")) .components(new Components() .addSecuritySchemes("bearerAuth", new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT"))) .addSecurityItem(new SecurityRequirement().addList("bearerAuth")); }

配置后UI右上角会出现Authorize按钮,填入token后所有请求都会自动带上Authorization头。这个功能对内部联调效率提升非常明显。

6. 从文档到契约:把OpenAPI用出更多价值

6.1 用描述文件生成前端请求代码

OpenAPI描述文件不只是给人看的,还能直接生成前端调用代码。我们用openapi-generator-cli,一条命令就能根据描述文件生成TypeScript的API客户端:

openapi-generator-cli generate \ -i openapi.json \ -g typescript-axios \ -o ./src/api

生成的代码包含所有接口的封装、请求参数类型、响应类型,前端直接import就能用。这样接口一改,重新生成一次,类型不匹配的地方编译期就报错,比运行时才发现问题强太多。这一步把文档从"参考材料"升级成了"契约",价值完全不一样了。

6.2 基于描述文件做接口mock

后端接口还没写完,前端要先行开发怎么办?用描述文件起一个mock服务就行。工具很多,原理都是读OpenAPI文件,按schema生成符合结构的假数据。我们用的是prism:

prism mock openapi.json

它会启动一个本地服务,所有接口都返回符合schema的示例数据。前端可以完全按真实接口的方式去调,等后端写完直接切地址即可。这个做法让前后端并行开发真正落地,而不是停留在口号上。

6.3 契约测试防止接口悄悄变更

最怕的情况是后端改了接口但没通知,前端上线才发现。我们的做法是在CI里加一步契约测试:把当前生成的描述文件和上一个发布版本的描述文件做diff,如果有破坏性变更(比如删了字段、改了类型、加了必填参数),就报警并要求人工确认。

破坏性变更的判定规则可以自己定,我们用的是这几条:

  • 删除已有字段
  • 字段类型发生变化
  • 新增必填参数
  • 删除已有接口路径
  • 响应状态码减少

非破坏性的变更(新增可选字段、新增接口)则允许直接通过。这套机制运行半年,成功拦下了三次可能导致线上故障的接口变更。

7. 一些关于长期维护的实在话

整套方案跑下来,我最大的体会是:工具能解决"文档怎么生成",但解决不了"文档愿不愿意维护"。注解写在代码里,改代码时顺手就改了,这是它比手写文档强的地方。但如果团队没有把文档质量纳入流程,再好的工具也会被绕过。

我们后来定了几条规矩,效果不错。第一,任何新增接口的合并请求,必须包含完整的注解,CI会卡。第二,接口有破坏性变更时,必须在合并请求描述里说明影响范围。第三,每个季度做一次文档巡检,把长期没人访问的接口标记出来,确认是否还需要保留。这些规矩不复杂,但坚持下来,文档的可用性就稳住了。

另外提醒一句,别追求一步到位。我们最开始只要求接口有summary,后来才逐步加上description、example、错误码说明。如果一开始就要求面面俱到,团队会觉得负担太重而抵触。循序渐进,让习惯先建立起来,再谈质量提升。

最后分享一个我们内部用的小技巧:把Swagger UI的地址做成二维码贴在工位上,对接方来问接口时直接让对方扫码自己看。省下来的沟通时间,比想象中多得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询