本文围绕
apidoc在Spring Boot项目中的接入、维护与交付展开,重点说明它适合解决的问题域、与springdoc / Swagger、Spring REST Docs的差异、源码注释应承担的粒度,以及怎样把静态接口文档纳入Jenkins构建产物链路。
讨论重点不是工具安装本身,而是工程化边界:业务接口持续演进时,怎样让文档具备可维护性、可追溯性和可发布性;哪些信息适合写入
apidoc,哪些信息仍然需要由测试、网关配置或运行时平台补位。
参考资料:
apidoc 官方资料:APIDOC Official Site 、 apiDoc Params 、 apidoc GitHub Repository
Spring 生态资料:springdoc-openapi 、 Spring REST Docs Reference Documentation 、 Spring Boot Reference Documentation
Dubbo 资料:Apache Dubbo Spring Boot 、 Apache Dubbo Lightweight Java SDK
Jenkins 资料:Jenkins Pipeline Syntax 、 Jenkins archiveArtifacts Step 、 Jenkins HTML Publisher Plugin 、 NodeJS Jenkins Plugin
IDEA 插件资料:apiDoc Plugin for IntelliJ IDEA
[TOC]
一、问题域与结论
围绕 apidoc 的工程落地,通常需要先回答五个问题:
apidoc的定位是什么,它与Swagger UI这类运行时文档方案有什么差异Spring Boot项目为什么还会选择apidoc- 注释式接口文档应维护到什么粒度,才能避免沦为重复劳动
- 文档生成动作怎样与
Jenkins的编译、测试、归档和发布过程衔接 - 这一方案适合哪些团队,不适合哪些团队
核心结论可以概括为:
apidoc本质上是一个基于源码注释扫描的静态 API 文档生成器。它不依赖应用启动时的控制器反射结果,也不要求对外暴露/v3/api-docs之类的运行时端点,而是把约定格式的注释块编译为一套 HTML 静态站点。
这个定位会带来两个直接推论:
- 优势在于静态、轻量、生成结果稳定,适合纳入 CI/CD 流程并按构建版本归档
- 代价在于文档质量高度依赖人工维护注释,注释与代码一旦脱节,产物就会过期
因此,理解 apidoc 的关键不在于把它视为“接口管理平台”,而在于把它视为“源码旁路的静态文档构建器”。
二、为什么在 Spring Boot 里还要看 apidoc
在 Java 后端团队中,接口文档常见方案通常有三类:
springdoc-openapi + Swagger UISpring REST Docsapidoc
三者解决的问题并不相同。
| 方案 | 文档来源 | 是否依赖应用启动 | 产物形态 | 优势 | 主要短板 |
|---|---|---|---|---|---|
springdoc-openapi |
注解 + 运行时扫描 | 是 | OpenAPI + Swagger UI | 集成 Spring Boot 自然,调试体验好 | 依赖运行中应用,偏在线化 |
Spring REST Docs |
测试用例 + 片段生成 | 通常需要测试执行 | AsciiDoc/HTML | 文档与测试强绑定,准确性高 | 初始接入成本较高 |
apidoc |
源码注释 | 否 | 静态 HTML | 静态构建简单,适合归档与发布 | 需要人工维护注释,约束弱时容易过期 |
若从 Spring Boot 生态主流程度判断,springdoc 更自然;若从“文档与测试结果强绑定”判断,Spring REST Docs 更稳。apidoc 之所以仍然有价值,通常来自以下现实需求:
- 团队需要一份独立于运行环境的静态文档,便于挂到 Jenkins、Nginx 或制品库
- 文档查看过程不应依赖服务启动,也不希望暴露文档端点
- 项目已有清晰的接口注释习惯,希望以较低成本沉淀为可浏览文档
- 文档产物更接近“版本化发布说明”,而不是在线调试入口
从定位上看,apidoc 更适合充当“构建阶段生成的接口说明书”,而不是“运行阶段暴露的接口浏览器”。
三、apidoc 的工作机制
apidoc 的工作链路可以压缩成下面这张图:
graph LR
A[Spring Boot 源码] --> B[Controller 或契约层注释块]
B --> C[apidoc 扫描]
C --> D[apidoc.json 配置]
D --> E[静态 HTML 文档]
E --> F[Jenkins 归档]
E --> G[HTML 报告发布]
从机制上看,它主要完成三件事:
- 扫描指定目录下的源码文件
- 提取符合
@api语法的注释块 - 结合配置和模板生成静态页面
理解 apidoc 时最容易混淆的一点是:
apidoc不理解 Spring MVC 的真实路由匹配逻辑,它只理解注释中声明的文档结构。
因此,下列信息不会被它自动推断:
@Validated、@NotNull之类校验注解对应的约束语义- 复杂泛型响应体的真实字段结构
- 运行时拦截器追加的请求头
- Spring Security、Gateway 或网关注入的鉴权流程
这类信息若要出现在文档里,只能通过人工注释补全,或利用统一的 @apiDefine、@apiUse 做片段复用。
另一个需要明确的点是:生成结果中的 api_data.js、api_project.js 等文件属于构建产物,不应手工编辑;文档的真实来源始终是源码注释与 apidoc 配置。
四、Spring Boot 中怎么落地 apidoc
最小接入思路
apidoc 是一个 Node.js 工具,与 Spring Boot 并不直接耦合。常见的接入方式如下:
- Java 工程继续使用
Maven或Gradle - 在项目根目录额外放置
package.json - 通过
npm或npx执行apidoc - 把生成目录输出到
build/、target/或docs/
一个较常见的目录组织方式如下:
1
2
3
4
5
6
7
8
9
order-service
├── src/main/java
│ └── com/example/order/controller
├── apidoc
│ ├── apidoc.json
│ └── header.md
├── package.json
├── package-lock.json
└── Jenkinsfile
把 apidoc 配置文件与页头、页脚等文档资源收拢到单独目录,有三个直接收益:
- 配置集中,维护入口明确
- CI 命令更清晰
- 多模块扫描与后续扩展更容易演进
package.json 示例
1
2
3
4
5
6
7
8
9
10
{
"name": "order-service-apidoc",
"private": true,
"devDependencies": {
"apidoc": "^1.2.0"
},
"scripts": {
"docs:build": "apidoc -i src/main/java -o build/apidoc -c apidoc"
}
}
这种定义有几个工程化收益:
- 本地与 Jenkins 使用同一条命令,减少环境偏差
apidoc版本固定在项目内,而不是依赖某台机器的全局安装- 提交
package-lock.json后,npm ci可以保证构建节点上的依赖版本可复现
apidoc.json 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "Order Service API",
"version": "1.0.0",
"description": "Spring Boot 订单服务接口文档",
"title": "Order Service apidoc",
"url": "https://api.example.com",
"sampleUrl": "https://api.example.com",
"header": {
"title": "概述",
"filename": "header.md"
},
"template": {
"forceLanguage": "zh_cn",
"withCompare": true,
"withGenerator": true
}
}
这里需要额外注意三点:
url与sampleUrl最好写成真实环境或统一网关前缀,不要直接照抄localhostwithCompare只有在文档中持续维护@apiVersion时才真正有价值,否则无法形成有效的版本对比header、footer等附加内容适合写简介、约束说明或统一错误码约定,不适合堆入大段与接口无关的说明
注释写在什么位置
apidoc 并不局限于 Controller 层。它只扫描源码注释,因此理论上任何被扫描到的 Java 文件都可以承载文档;但从契约清晰度来看,最适合的仍然是“对外协议边界层”。
在 Spring Boot Web 场景中,较稳妥的写法是将注释放在 Controller 方法上方,原因在于这一层与 HTTP 语义最接近:
- 路径明确
- 请求方法明确
- 请求参数与响应语义最集中
- 前后端协作也主要围绕这一层展开
因此,对于典型的 Spring MVC / Spring Boot Web 应用,可以遵循一个简单原则:
- 对外是 HTTP 接口,优先写在
Controller
不建议默认把 apidoc 注释放在以下位置:
Service实现类Feign客户端接口Mapper层
这些位置虽然也能被扫描,但文档层级会与 HTTP 入口脱节。
Dubbo 场景下的放置位置
如果应用不是对外暴露 HTTP,而是一个纯 Dubbo Provider,文档放置位置就不应再沿用 Controller 思路,而应调整为:
- 文档写在 RPC 契约层
- 对
Dubbo来说,这个契约层通常就是 service interface
原因在于:
Dubbo调用双方共享的是接口契约,而不是实现类- Provider 对外暴露的核心是方法签名、参数语义与返回语义
- 实现类可能存在多个版本,并掺杂事务、缓存、日志、重试等内部细节
几种常见放置位置的对比如下:
| 放置位置 | Spring MVC 场景 | Dubbo 场景 | 是否推荐 | 原因 |
|---|---|---|---|---|
Controller |
很适合 | 不适用或不完整 | 视场景而定 | 只适合描述 HTTP 边界 |
Service 接口 |
一般不作为 HTTP 文档主入口 | 很适合 | 推荐用于 Dubbo | 这里才是 RPC 契约 |
Service 实现类 |
不推荐 | 一般不推荐 | 不推荐 | 容易混入实现细节,且不是共享契约 |
Feign / Reference 调用端 |
不推荐 | 不推荐 | 不推荐 | 它是消费方视角,不是服务定义本身 |
可以压缩为一句话:
HTTP文档优先写在Controller,Dubbo文档优先写在共享service interface;关键不在于“写在哪一层”,而在于“写在真正定义外部契约的那一层”。
Dubbo 写在 service interface 的示例
在很多 Dubbo 项目里,把 apidoc 写在共享接口层是更合理的做法,但这里的 Service 指的是“对外暴露的 RPC 接口”,而不是本地业务 Service 或内部装配 Service。
一个典型的 Dubbo 项目可能拆分为:
1
2
3
4
5
6
7
order-service
├── order-api
│ └── OrderQueryDubboService.java
├── order-provider
│ └── OrderQueryDubboServiceImpl.java
└── order-consumer
└── OrderFacade.java
最合适的文档位置通常是:
order-api模块中的OrderQueryDubboService.java
而不是:
order-provider中的OrderQueryDubboServiceImpl.java
因为前者才是 Provider 与 Consumer 共同依赖的接口定义。
Dubbo 场景的文档可写成如下形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface OrderQueryDubboService {
/**
* @api {dubbo} OrderQueryDubboService/getDetail 查询订单详情
* @apiName GetOrderDetailByDubbo
* @apiGroup Dubbo-Order
* @apiVersion 1.0.0
* @apiDescription Dubbo 服务:按订单 ID 查询订单详情,供订单聚合服务、售后服务调用。
*
* @apiParam {Long} orderId 订单 ID
*
* @apiSuccess {Long} id 订单 ID
* @apiSuccess {String} orderNo 订单编号
* @apiSuccess {BigDecimal} amount 订单金额
* @apiSuccess {Integer} status 订单状态
*
* @apiError {String} code 错误码
* @apiError {String} message 错误描述
*/
OrderDetailDTO getDetail(Long orderId);
}
其中 OrderQueryDubboService/getDetail 不是 Dubbo 的真实 URL,而是一种稳定的文档标识,用来表达“哪个服务的哪个方法”。
Dubbo 对外暴露 HTTP 语义时的拆分原则
如果服务最终会通过网关、Triple、HTTP 映射等方式被外部系统直接调用,文档宜拆成两层:
- 外部调用文档:描述 HTTP / 网关入口
- 内部 RPC 文档:描述 Dubbo interface
不应将两层语义强行揉成一份文档,否则容易混淆:
- 调用入口到底是
/api/orders/{id}还是OrderQueryDubboService/getDetail - 鉴权头属于网关语义还是 RPC 附件语义
- 错误码属于网关层还是 Provider 业务层
Dubbo 场景下的扫描目录
如果扫描目录只写成 src/main/java,在 Dubbo 多模块项目里往往不够精确,更适合按契约模块扫描。
例如:
1
2
3
4
5
{
"scripts": {
"docs:build": "apidoc -i order-api/src/main/java -o build/apidoc -c apidoc"
}
}
仓库中若包含多个 RPC 接口模块,也可以统一纳入扫描范围,但基本原则不变:
- 优先扫描 API 契约模块
- 不要默认以实现模块作为唯一文档源
一个通用判断标准
与其死记“写在 Controller 还是 Service”,更适合使用下面这个判断标准:
| 问题 | 如果答案是“是” | 说明适合写 apidoc |
|---|---|---|
| 这一层是否定义了对外调用契约 | 是 | 适合 |
| 调用方是否直接依赖这一层的签名或协议 | 是 | 适合 |
| 这一层是否混入大量内部实现细节 | 是 | 不适合 |
| 这一层是否只是内部编排,不直接暴露给调用方 | 是 | 不适合 |
IDEA 插件与 CLI 的分工
IntelliJ IDEA 插件市场里有现成的 apiDoc 插件,可以作为本地开发时的辅助工具。
这类插件的价值主要在于:
- 在 IDE 内快速跳转、查看或辅助维护注释
- 降低“回终端执行命令”的切换成本
- 提升本地编写
apidoc注释时的效率
但从工程角度看,IDEA 插件与 CLI / Jenkins 的职责需要分清:
| 维度 | IDEA 插件 | CLI / Jenkins |
|---|---|---|
| 使用场景 | 本地开发辅助 | 团队统一生成与发布 |
| 环境一致性 | 依赖个人 IDE 配置 | 更容易在团队内统一 |
| 最终产物可信度 | 适合本地预览或辅助 | 应作为最终文档生成标准 |
| 自动化能力 | 较弱 | 强,适合 CI/CD |
因此,更合理的实践是:
- 本地安装
apiDoc插件提升编辑体验 - 交付链路仍以
npm run docs:build与Jenkins结果为准
五、常用注释标签与语义边界
常用标签的心智模型如下:
| 标签 | 作用 | 在 Spring Boot 中的典型用途 |
|---|---|---|
@api |
定义请求方法与路径 | 标识 GET /orders/{id} 这类接口入口 |
@apiName |
定义接口唯一名称 | 生成页面锚点,便于区分同组接口 |
@apiGroup |
接口分组 | 常按业务域如 Order、User 分组 |
@apiDescription |
描述接口用途 | 解释业务动作,而非重复方法名 |
@apiParam |
描述路径参数或通用参数 | 适合 path variable、通用参数块 |
@apiQuery |
描述查询字符串参数 | 适合列表查询、筛选条件、分页参数 |
@apiBody |
描述请求体字段 | 适合 POST / PUT 请求体结构 |
@apiHeader |
描述请求头 | 适合 Authorization、租户头、链路头 |
@apiSuccess |
描述成功响应字段 | 说明响应结构和业务字段含义 |
@apiError |
描述失败响应字段 | 统一异常码和失败场景 |
@apiParamExample |
给出入参示例 | 展示查询参数或请求体样例 |
@apiSuccessExample |
给出成功响应示例 | 展示 JSON 返回结构 |
@apiErrorExample |
给出失败响应示例 | 展示业务错误或鉴权失败样例 |
@apiVersion |
版本标记 | 适合接口升级或兼容多版本 |
@apiDefine / @apiUse |
公共片段复用 | 复用统一鉴权头、统一响应结构 |
@apiDeprecated |
标记废弃接口 | 用于版本过渡期的兼容说明 |
对于日常业务接口,文档真正需要回答的是三个问题:
- 调用方需要传什么
- 正常情况下返回什么
- 失败时会因为什么报错
如果这三件事没有讲清,页面结构再完整,文档依然是空心的。
需要特别区分的一点是:
@apiParam更适合描述路径参数或抽象的公共参数定义@apiQuery更适合描述查询字符串@apiBody更适合描述请求体
把三类参数拆开写,页面会更接近真实调用语义,也更便于前后端协作与测试排查。
六、一个 Spring Boot 接口的实际写法
下面以订单查询接口为例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderApplicationService orderApplicationService;
public OrderController(OrderApplicationService orderApplicationService) {
this.orderApplicationService = orderApplicationService;
}
/**
* @api {get} /api/orders/{orderId} 查询订单详情
* @apiName GetOrderDetail
* @apiGroup Order
* @apiVersion 1.0.0
* @apiDescription 按订单号查询订单详情,返回订单主信息、金额和状态。
*
* @apiHeader {String} Authorization Bearer token
*
* @apiParam {Long} orderId 订单 ID
*
* @apiSuccess {String} code 响应码,成功时固定为 0
* @apiSuccess {String} message 响应消息
* @apiSuccess {Object} data 业务数据
* @apiSuccess {Long} data.id 订单 ID
* @apiSuccess {String} data.orderNo 订单编号
* @apiSuccess {BigDecimal} data.amount 订单金额
* @apiSuccess {String} data.status 订单状态,取值为 CREATED、PAID、CANCELLED
*
* @apiError {String} code 响应码
* @apiError {String} message 错误描述
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "code": "0",
* "message": "success",
* "data": {
* "id": 1024,
* "orderNo": "SO202606230001",
* "amount": 99.50,
* "status": "PAID"
* }
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 404 Not Found
* {
* "code": "ORDER_NOT_FOUND",
* "message": "订单不存在"
* }
*/
@GetMapping("/{orderId}")
public CommonResponse<OrderDetailVO> getOrderDetail(@PathVariable Long orderId) {
return CommonResponse.success(orderApplicationService.getDetail(orderId));
}
}
这个写法有几个值得明确的点:
- 文档路径宜写完整路径,避免多层
@RequestMapping叠加后由读者自行拼接 - 响应结构不应只写
data,应展开前端真正关心的字段 - 错误响应不宜只写“系统异常”,至少需要标出最常见的业务失败场景
复杂请求体的写法
如果是创建订单这类 POST 接口,可以写成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* @api {post} /api/orders 创建订单
* @apiName CreateOrder
* @apiGroup Order
* @apiVersion 1.0.0
* @apiDescription 创建一个新订单,请求体包含用户 ID、商品列表和配送地址。
*
* @apiHeader {String} Authorization Bearer token
*
* @apiBody {Long} userId 用户 ID
* @apiBody {Object[]} items 商品明细
* @apiBody {Long} items.skuId 商品 SKU ID
* @apiBody {Integer} items.quantity 购买数量
* @apiBody {String} address 收货地址
*
* @apiParamExample {json} Request-Example:
* {
* "userId": 20001,
* "items": [
* {
* "skuId": 3001001,
* "quantity": 2
* }
* ],
* "address": "上海市浦东新区示例路 100 号"
* }
*
* @apiSuccess {String} code 响应码
* @apiSuccess {String} message 响应消息
* @apiSuccess {Object} data 业务数据
* @apiSuccess {Long} data.id 新建订单 ID
* @apiSuccess {String} data.orderNo 新建订单编号
*/
这里最关键的经验是:
DTO 再复杂,文档也不应只写“见
CreateOrderRequest类”。apidoc不是运行时 schema 推导工具,不会自动展开对象结构,因此关键字段必须显式写出。
对于枚举值、时间格式、金额单位等容易产生歧义的字段,也应直接写在说明中,而不是假定调用方会去翻源码。
七、复用公共文档片段比重复堆注释更重要
项目接口数量一多,最先失控的往往不是生成命令,而是重复注释。所有接口都反复声明同一组请求头、统一响应壳和分页字段,维护成本会迅速膨胀。
此时应优先使用 @apiDefine 与 @apiUse。
定义公共片段
1
2
3
4
5
6
7
8
9
10
11
/**
* @apiDefine AuthHeader
* @apiHeader {String} Authorization Bearer token
*/
/**
* @apiDefine CommonSuccess
* @apiSuccess {String} code 响应码,成功时固定为 0
* @apiSuccess {String} message 响应消息
* @apiSuccess {Object} data 业务数据
*/
在接口上复用
1
2
3
4
5
6
7
/**
* @api {get} /api/orders/{orderId} 查询订单详情
* @apiName GetOrderDetail
* @apiGroup Order
* @apiUse AuthHeader
* @apiUse CommonSuccess
*/
这样做的意义不只是减少重复文本,更重要的是让文档结构与接口规范真正统一。
更贴近真实后台项目的公共定义
对中后台项目而言,跨接口重复出现的往往不只是 Authorization 与 code/message/data,还包括链路追踪头、多租户头、统一成功壳、统一失败壳和分页结构。
| 维度 | 常见做法 | 文档中必须说明的内容 |
|---|---|---|
| 鉴权 | Authorization: Bearer xxx |
token 类型、是否必传、失效时返回码 |
| 链路追踪 | X-Trace-Id |
是否可透传、由谁生成 |
| 多租户 | X-Tenant-Id |
哪些后台接口必传 |
| 统一响应 | code/message/success/data/requestId |
成功失败语义,不应只写 data |
| 分页列表 | records/total/pageNum/pageSize/pages |
字段命名与前端契约必须一致 |
可以进一步定义为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* @apiDefine AdminAuthHeader
* @apiHeader {String} Authorization Bearer access token
* @apiHeader {String} X-Trace-Id 链路追踪 ID,便于日志排查
* @apiHeader {String} X-Tenant-Id 租户 ID,多租户后台场景必传
*/
/**
* @apiDefine CommonResult
* @apiSuccess {Integer} code 业务响应码,0 表示成功
* @apiSuccess {Boolean} success 是否成功
* @apiSuccess {String} message 响应消息
* @apiSuccess {String} requestId 服务端生成的请求 ID
* @apiSuccess {Object} data 业务数据
*/
/**
* @apiDefine PageResult
* @apiSuccess {Object} data 分页结果
* @apiSuccess {Object[]} data.records 当前页数据列表
* @apiSuccess {Long} data.total 总条数
* @apiSuccess {Integer} data.pageNum 当前页码
* @apiSuccess {Integer} data.pageSize 每页条数
* @apiSuccess {Integer} data.pages 总页数
*/
/**
* @apiDefine CommonError
* @apiError {Integer} code 错误码
* @apiError {Boolean} success 是否成功,失败时固定为 false
* @apiError {String} message 错误消息
* @apiError {String} requestId 请求 ID
*/
这一层的核心在于:先把“每个接口都重复出现的协议壳”抽出来,再让具体接口只描述自己的业务字段。
后台分页接口的写法
下面是一个更贴近中后台项目的“订单分页查询”接口示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* @api {get} /api/admin/orders 分页查询订单
* @apiName PageQueryOrder
* @apiGroup Admin-Order
* @apiVersion 1.0.0
* @apiDescription 按订单号、状态、时间范围分页查询订单列表,适用于运营后台和客服后台。
*
* @apiUse AdminAuthHeader
* @apiUse CommonResult
* @apiUse PageResult
* @apiUse CommonError
*
* @apiQuery {Integer} pageNum 页码,从 1 开始
* @apiQuery {Integer} pageSize 每页条数,常用 10、20、50
* @apiQuery {String} [orderNo] 订单号,支持精确查询
* @apiQuery {Integer} [status] 订单状态,0-待支付,1-已支付,2-已取消,3-已完成
* @apiQuery {String} [userId] 用户 ID
* @apiQuery {String} [startTime] 创建开始时间,格式 `yyyy-MM-dd HH:mm:ss`
* @apiQuery {String} [endTime] 创建结束时间,格式 `yyyy-MM-dd HH:mm:ss`
*
* @apiSuccess {Object[]} data.records 当前页列表
* @apiSuccess {Long} data.records.id 订单 ID
* @apiSuccess {String} data.records.orderNo 订单编号
* @apiSuccess {Long} data.records.userId 下单用户 ID
* @apiSuccess {BigDecimal} data.records.payAmount 支付金额
* @apiSuccess {Integer} data.records.status 订单状态
* @apiSuccess {String} data.records.createTime 创建时间
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "code": 0,
* "success": true,
* "message": "success",
* "requestId": "f8b0bdb2d43a4f56",
* "data": {
* "records": [
* {
* "id": 1024,
* "orderNo": "SO202606230001",
* "userId": 20001,
* "payAmount": 99.50,
* "status": 1,
* "createTime": "2026-06-23 10:30:00"
* }
* ],
* "total": 128,
* "pageNum": 1,
* "pageSize": 20,
* "pages": 7
* }
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 401 Unauthorized
* {
* "code": 40101,
* "success": false,
* "message": "登录状态已失效",
* "requestId": "f8b0bdb2d43a4f56"
* }
*/
@GetMapping("/api/admin/orders")
public CommonResponse<PageResult<OrderPageVO>> pageQuery(OrderPageQuery query) {
return CommonResponse.success(orderQueryService.pageQuery(query));
}
这个例子更贴近实际项目,主要体现在三点:
- 包含后台列表页最常见的分页协议
- 显式体现了鉴权、多租户和链路追踪头
- 展开了筛选条件与列表字段,而不是只给出一个抽象 DTO 名称
版本对比与废弃管理
apidoc 的 withCompare 价值,建立在持续维护 @apiVersion 的前提下。若接口版本从 1.0.0 演进到 1.1.0,并且字段或错误码发生变化,页面才能展示版本差异。
版本维护可以遵循以下原则:
- 不破坏兼容性的字段补充,升级次版本,例如
1.0.0 -> 1.1.0 - 明确破坏兼容性的调整,升级主版本,例如
1.x -> 2.0.0 - 已进入迁移期但仍保留的接口,可加
@apiDeprecated - 真正删除旧接口前,应先保证旧构建产物仍可回溯
对团队而言,最实用的价值不只是页面上的对比高亮,而是“每一次接口演进都能映射到一份可追踪的静态文档产物”。
八、实战案例:订单服务接入 apidoc
以下案例更接近真实工程场景。
场景设定
假设存在一个 Spring Boot 订单服务,提供以下接口:
GET /api/orders/{orderId}查询订单详情POST /api/orders创建订单POST /api/orders/{orderId}/pay发起支付POST /api/orders/{orderId}/cancel取消订单
团队希望达到以下目标:
- 每次主干构建自动生成最新接口文档
- 文档作为构建产物保留,便于回溯历史版本
- Jenkins 页面可以直接打开 HTML 文档
- 生成失败时构建失败,避免“接口已变更但文档未更新”
推荐的工程约束
| 约束项 | 推荐做法 | 原因 |
|---|---|---|
| 文档注释位置 | 只写在 Controller 入口方法上 | 保持和 HTTP 语义一致 |
| 公共头信息 | 用 @apiDefine 抽取 |
减少重复 |
| 输出目录 | build/apidoc 或 target/apidoc |
便于 Jenkins 归档 |
| 生成命令 | 固定为 npm run docs:build |
本地与 CI 统一 |
| 版本策略 | 跟随服务版本或网关版本 | 文档可回溯 |
| 发布策略 | Jenkins HTML 报告 + 归档 ZIP | 兼顾浏览与留档 |
本地生成命令
1
2
npm ci
npm run docs:build
生成成功后,通常会得到如下目录:
1
2
3
4
5
6
build/apidoc
├── api_data.js
├── api_data.json
├── api_project.js
├── index.html
└── vendor/
其中最核心的是:
index.html:页面入口api_data.*:接口元数据api_project.js:项目信息
从交付视角看,build/apidoc/ 整个目录就是最终产物。
图片目录与命名约定
为了便于文章继续扩展,相关图片目录可固定为:
1
2
3
4
5
6
asserts/images/2026-06-13-apidoc学习笔记/
├── SpringBoot项目中的apidoc落位关系.svg
├── Jenkins生成并发布apidoc流程图.svg
├── apidoc页面结构示意图.svg
├── apidoc首页截图.png
└── Jenkins-HTML报告页截图.png
文中已经引用了结构关系图与页面结构示意图。若后续继续补图,优先级较高的两张截图是:
apidoc首页截图.png:展示分组导航、请求参数区、响应示例区Jenkins-HTML报告页截图.png:展示构建完成后的 HTML 报告入口
九、Jenkins 怎么配合 apidoc
这部分是工程落地的重点。
Jenkins 在链路中的职责
Jenkins 的职责不只是“多执行一条 apidoc 命令”,而是承担四项工作:
- 统一构建环境
- 在编译和测试之后生成文档
- 把文档归档为可追溯构建产物
- 把 HTML 文档发布到 Jenkins 页面
整个流程可以表示为:
graph TD
A[代码提交] --> B[Jenkins Checkout]
B --> C[Maven 编译测试]
C --> D[npm ci]
D --> E[apidoc 生成 HTML]
E --> F[archiveArtifacts 归档]
E --> G[publishHTML 发布报告]
如果需要一张与截图体系一致的静态图,也可以配合使用:
Jenkins 前置条件
如果使用 Declarative Pipeline,通常需要准备以下环境:
| 项目 | 说明 |
|---|---|
| JDK | 用于构建 Spring Boot |
| Maven 或 Gradle | 用于 Java 构建 |
| Node.js | 用于执行 apidoc |
| HTML Publisher Plugin | 用于在 Jenkins 页面展示 HTML 文档 |
代码仓库中的 Jenkinsfile |
用于把构建流程脚本化 |
这里最容易遗漏的一点是:
apidoc是 Node 工具,因此即使后端项目是纯 Java,也需要为 Jenkins agent 准备 Node 运行环境。
很多团队第一次接入失败,并不是 apidoc 配置错误,而是构建节点只有 JDK + Maven,缺少 Node.js。
Jenkinsfile 示例
Declarative Pipeline 可以采用如下结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
pipeline {
agent any
tools {
jdk 'jdk17'
maven 'maven-3.9'
nodejs 'node-20'
}
options {
timestamps()
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
}
environment {
DOC_OUTPUT = 'build/apidoc'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build And Test') {
steps {
sh 'mvn -B clean test'
}
}
stage('Install Doc Dependencies') {
steps {
sh 'npm ci'
}
}
stage('Generate API Doc') {
steps {
sh 'npm run docs:build'
}
}
stage('Verify API Doc') {
steps {
sh 'test -f build/apidoc/index.html'
}
}
}
post {
always {
archiveArtifacts artifacts: 'build/apidoc/**', fingerprint: true
junit 'target/surefire-reports/*.xml'
}
success {
publishHTML(target: [
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'build/apidoc',
reportFiles: 'index.html',
reportName: 'API Documentation',
reportTitles: 'apidoc'
])
}
}
}
这份 Pipeline 的设计意图可以拆解为:
Build And Test在文档生成之前执行,先保证代码本身通过基本校验npm ci基于锁文件安装依赖,减少构建漂移Verify API Doc显式校验index.html是否存在,避免“命令执行了但产物为空”archiveArtifacts无论成功与否都执行,便于排查失败现场publishHTML只在成功后发布,避免 Jenkins 页面挂出半成品
没有 NodeJS Plugin 时的处理方式
有些 Jenkins 环境没有 NodeJS Plugin,这时常见做法有两类:
- 使用已预装
Node.js的构建节点 - 使用 Docker agent,将
JDK + Maven + Node统一封装到同一个镜像
如果 Jenkins 已全面容器化构建,第二种做法通常更利于环境复现与跨节点迁移。
常见失败点
apidoc 接入 Jenkins 后,常见失败点通常集中在以下方面:
| 失败点 | 现象 | 原因 |
|---|---|---|
| Node 未安装 | apidoc: command not found |
Jenkins agent 环境缺失 |
| 扫描目录不对 | 文档为空或接口缺失 | -i 路径未覆盖实际 Controller 或契约层 |
| 注释块格式错误 | 某个接口未生成 | @api 语法不完整或注释结构被破坏 |
| 输出目录不一致 | publishHTML 找不到文件 |
apidoc 输出目录与 Jenkins 配置不一致 |
| 锁文件缺失 | npm ci 失败 |
未提交 package-lock.json |
| 测试先失败 | 没有文档产物 | Pipeline 在前置阶段就中断 |
因此,在 CI 中接入 apidoc 时,除生成命令外,还应把“生成目录是否存在、index.html 是否生成”纳入显式检查项。
十、怎样把这个方案真正用顺手
单纯“能生成”并不等于“能长期维护”。要让 apidoc 在团队内真正稳定运行,至少需要补齐以下约束。
约束一:接口改动必须同步修改注释
只要采用注释式文档,最大的风险就是文档过期。因此在代码评审阶段,最好显式检查:
- 新增接口是否补充了
apidoc注释 - 请求参数、返回结构变更时是否同步更新文档
- 错误码变更时是否同步更新文档
如果团队已经使用 PR 模板,可以把“是否同步更新 API 文档”设为固定检查项。
约束二:公共结构必须抽象复用
不应让每个接口都重复书写以下内容:
- 登录态请求头
- 统一响应结构
- 分页字段
- 通用业务错误
这些内容一旦不抽象,维护成本会随接口规模快速膨胀。
约束三:文档只写对外语义,不写内部实现细节
一个常见误区是把接口文档写成“后端实现说明书”,例如:
- 调用了哪个 Service
- 查了哪张表
- 经过了哪个 MQ
这些信息对调用方通常没有直接价值,反而会让文档失焦。接口文档更应聚焦:
- 调用入口
- 参数约束
- 返回结构
- 业务语义
- 常见错误
约束四:版本号与废弃策略一起维护
如果页面开启了 withCompare,但团队从不维护 @apiVersion,版本对比功能就失去意义。更可取的做法是:
- 接口有兼容性变化时同步调整
@apiVersion - 即将下线的旧接口用
@apiDeprecated标记过渡期 - 保留 Jenkins 历史构建产物,作为旧版本文档回溯入口
这样处理后,文档才不仅是“当前快照”,而是“接口演化历史”的一部分。
十一、apidoc 的边界和不足
只谈优点并不足以支撑技术选型,apidoc 的边界同样需要明确。
它不擅长“自动正确”
springdoc 的优势之一,是许多结构来自运行时扫描与注解推导;Spring REST Docs 的优势之一,是文档由测试驱动生成。
相比之下,apidoc 更依赖人工维护,因此天然不擅长:
- 自动同步 DTO 字段变化
- 自动反映参数校验规则
- 自动从测试结果校验示例是否真实
它更适合“稳定静态产物”,不更适合“在线调试入口”
如果团队真正需要的是:
- 在线 Try it out
- OpenAPI 规范导出
- 客户端代码生成
- 网关与接口平台联动
那么 apidoc 并不是最自然的方案,OpenAPI 体系通常更合适。
它对注释规范的要求比想象中更高
一个人维护十个接口时,手写注释并不困难;十个人维护两百个接口时,如果没有统一规范,页面很容易出现以下问题:
- 同一字段有多种叫法
- 错误码风格不一致
- 示例请求与真实逻辑脱节
- 分组混乱
因此,apidoc 真正依赖的不是工具本身,而是团队的文档纪律。
十二、什么时候适合选 apidoc
apidoc 的适用场景可以概括如下:
| 场景 | 是否适合 | 原因 |
|---|---|---|
| 需要生成静态 HTML 并归档到 Jenkins | 很适合 | 产物天然就是静态站点 |
| 不希望暴露运行时文档接口 | 很适合 | 完全离线生成 |
| 团队已习惯在源码旁维护接口说明 | 适合 | 接入成本较低 |
| 需要 OpenAPI 规范供平台联动 | 不太适合 | 原生能力不强 |
| 需要文档与测试强绑定 | 不太适合 | Spring REST Docs 更合适 |
| 需要自动推导 DTO schema | 不太适合 | springdoc 更自然 |
更直白的判断方式是:
当团队更在乎“静态版本化文档构建”和“Jenkins 中的统一发布”时,
apidoc值得采用;当团队更在乎“生态兼容性、自动化推导、在线调试与规范联动”时,优先考虑OpenAPI体系通常更合理。
十三、总结
把 apidoc 放进 Spring Boot + Jenkins 这一组合中观察,它的价值可以概括为三点:
- 适合把接口说明当作构建产物来交付
- 适合在不暴露运行时文档接口的前提下生成静态 HTML
- 上限取决于团队是否真的把注释规范纳入日常开发与评审流程
一套较稳妥的落地方式通常包括:
- 在
Controller或共享契约层维护apidoc注释 - 通过
package.json固定docs:build命令 - 由
Jenkinsfile统一执行mvn test + npm ci + apidoc - 通过
archiveArtifacts + publishHTML完成留档与展示
如果只需要短期生成一个可浏览页面,这套方案并不复杂;但要让它长期可维护,重点始终不在“安装了 apidoc”,而在于以下三点是否持续执行:
- 接口变更是否必须同步更新注释
- 公共文档片段是否被统一抽象
- 每次 Jenkins 构建是否真实地产出并发布文档
做到这三点,apidoc 才会从一个文档生成命令,演进为 Spring Boot 项目交付链路中的稳定组成部分。