apidoc 接口文档

从 Spring Boot 注释式文档到 Jenkins 自动生成发布,系统梳理 apidoc 的接入方式与工程化落地

Posted by Ekko on June 13, 2026

本文围绕 apidocSpring Boot 项目中的接入、维护与交付展开,重点说明它适合解决的问题域、与 springdoc / SwaggerSpring REST Docs 的差异、源码注释应承担的粒度,以及怎样把静态接口文档纳入 Jenkins 构建产物链路。

讨论重点不是工具安装本身,而是工程化边界:业务接口持续演进时,怎样让文档具备可维护性、可追溯性和可发布性;哪些信息适合写入 apidoc,哪些信息仍然需要由测试、网关配置或运行时平台补位。

参考资料:

apidoc 官方资料:APIDOC Official SiteapiDoc Paramsapidoc GitHub Repository

Spring 生态资料:springdoc-openapiSpring REST Docs Reference DocumentationSpring Boot Reference Documentation

Dubbo 资料:Apache Dubbo Spring BootApache Dubbo Lightweight Java SDK

Jenkins 资料:Jenkins Pipeline SyntaxJenkins archiveArtifacts StepJenkins HTML Publisher PluginNodeJS Jenkins Plugin

IDEA 插件资料:apiDoc Plugin for IntelliJ IDEA

[TOC]


一、问题域与结论

围绕 apidoc 的工程落地,通常需要先回答五个问题:

  1. apidoc 的定位是什么,它与 Swagger UI 这类运行时文档方案有什么差异
  2. Spring Boot 项目为什么还会选择 apidoc
  3. 注释式接口文档应维护到什么粒度,才能避免沦为重复劳动
  4. 文档生成动作怎样与 Jenkins 的编译、测试、归档和发布过程衔接
  5. 这一方案适合哪些团队,不适合哪些团队

核心结论可以概括为:

apidoc 本质上是一个基于源码注释扫描的静态 API 文档生成器。它不依赖应用启动时的控制器反射结果,也不要求对外暴露 /v3/api-docs 之类的运行时端点,而是把约定格式的注释块编译为一套 HTML 静态站点。

这个定位会带来两个直接推论:

  • 优势在于静态、轻量、生成结果稳定,适合纳入 CI/CD 流程并按构建版本归档
  • 代价在于文档质量高度依赖人工维护注释,注释与代码一旦脱节,产物就会过期

因此,理解 apidoc 的关键不在于把它视为“接口管理平台”,而在于把它视为“源码旁路的静态文档构建器”。


二、为什么在 Spring Boot 里还要看 apidoc

在 Java 后端团队中,接口文档常见方案通常有三类:

  1. springdoc-openapi + Swagger UI
  2. Spring REST Docs
  3. apidoc

三者解决的问题并不相同。

方案 文档来源 是否依赖应用启动 产物形态 优势 主要短板
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 报告发布]

从机制上看,它主要完成三件事:

  1. 扫描指定目录下的源码文件
  2. 提取符合 @api 语法的注释块
  3. 结合配置和模板生成静态页面

理解 apidoc 时最容易混淆的一点是:

apidoc 不理解 Spring MVC 的真实路由匹配逻辑,它只理解注释中声明的文档结构。

因此,下列信息不会被它自动推断:

  • @Validated@NotNull 之类校验注解对应的约束语义
  • 复杂泛型响应体的真实字段结构
  • 运行时拦截器追加的请求头
  • Spring Security、Gateway 或网关注入的鉴权流程

这类信息若要出现在文档里,只能通过人工注释补全,或利用统一的 @apiDefine@apiUse 做片段复用。

另一个需要明确的点是:生成结果中的 api_data.jsapi_project.js 等文件属于构建产物,不应手工编辑;文档的真实来源始终是源码注释与 apidoc 配置。


四、Spring Boot 中怎么落地 apidoc

最小接入思路

apidoc 是一个 Node.js 工具,与 Spring Boot 并不直接耦合。常见的接入方式如下:

  1. Java 工程继续使用 MavenGradle
  2. 在项目根目录额外放置 package.json
  3. 通过 npmnpx 执行 apidoc
  4. 把生成目录输出到 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
  }
}

这里需要额外注意三点:

  • urlsampleUrl 最好写成真实环境或统一网关前缀,不要直接照抄 localhost
  • withCompare 只有在文档中持续维护 @apiVersion 时才真正有价值,否则无法形成有效的版本对比
  • headerfooter 等附加内容适合写简介、约束说明或统一错误码约定,不适合堆入大段与接口无关的说明

注释写在什么位置

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

原因在于:

  1. Dubbo 调用双方共享的是接口契约,而不是实现类
  2. Provider 对外暴露的核心是方法签名、参数语义与返回语义
  3. 实现类可能存在多个版本,并掺杂事务、缓存、日志、重试等内部细节

几种常见放置位置的对比如下:

放置位置 Spring MVC 场景 Dubbo 场景 是否推荐 原因
Controller 很适合 不适用或不完整 视场景而定 只适合描述 HTTP 边界
Service 接口 一般不作为 HTTP 文档主入口 很适合 推荐用于 Dubbo 这里才是 RPC 契约
Service 实现类 不推荐 一般不推荐 不推荐 容易混入实现细节,且不是共享契约
Feign / Reference 调用端 不推荐 不推荐 不推荐 它是消费方视角,不是服务定义本身

可以压缩为一句话:

HTTP 文档优先写在 ControllerDubbo 文档优先写在共享 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:buildJenkins 结果为准

五、常用注释标签与语义边界

常用标签的心智模型如下:

标签 作用 在 Spring Boot 中的典型用途
@api 定义请求方法与路径 标识 GET /orders/{id} 这类接口入口
@apiName 定义接口唯一名称 生成页面锚点,便于区分同组接口
@apiGroup 接口分组 常按业务域如 OrderUser 分组
@apiDescription 描述接口用途 解释业务动作,而非重复方法名
@apiParam 描述路径参数或通用参数 适合 path variable、通用参数块
@apiQuery 描述查询字符串参数 适合列表查询、筛选条件、分页参数
@apiBody 描述请求体字段 适合 POST / PUT 请求体结构
@apiHeader 描述请求头 适合 Authorization、租户头、链路头
@apiSuccess 描述成功响应字段 说明响应结构和业务字段含义
@apiError 描述失败响应字段 统一异常码和失败场景
@apiParamExample 给出入参示例 展示查询参数或请求体样例
@apiSuccessExample 给出成功响应示例 展示 JSON 返回结构
@apiErrorExample 给出失败响应示例 展示业务错误或鉴权失败样例
@apiVersion 版本标记 适合接口升级或兼容多版本
@apiDefine / @apiUse 公共片段复用 复用统一鉴权头、统一响应结构
@apiDeprecated 标记废弃接口 用于版本过渡期的兼容说明

对于日常业务接口,文档真正需要回答的是三个问题:

  1. 调用方需要传什么
  2. 正常情况下返回什么
  3. 失败时会因为什么报错

如果这三件事没有讲清,页面结构再完整,文档依然是空心的。

需要特别区分的一点是:

  • @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
 */

这样做的意义不只是减少重复文本,更重要的是让文档结构与接口规范真正统一。

更贴近真实后台项目的公共定义

对中后台项目而言,跨接口重复出现的往往不只是 Authorizationcode/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 名称

版本对比与废弃管理

apidocwithCompare 价值,建立在持续维护 @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 取消订单

团队希望达到以下目标:

  1. 每次主干构建自动生成最新接口文档
  2. 文档作为构建产物保留,便于回溯历史版本
  3. Jenkins 页面可以直接打开 HTML 文档
  4. 生成失败时构建失败,避免“接口已变更但文档未更新”

Spring Boot 项目中的 apidoc 落位关系.svg

推荐的工程约束

约束项 推荐做法 原因
文档注释位置 只写在 Controller 入口方法上 保持和 HTTP 语义一致
公共头信息 @apiDefine 抽取 减少重复
输出目录 build/apidoctarget/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 报告入口

apidoc 页面结构示意图.svg


九、Jenkins 怎么配合 apidoc

这部分是工程落地的重点。

Jenkins 在链路中的职责

Jenkins 的职责不只是“多执行一条 apidoc 命令”,而是承担四项工作:

  1. 统一构建环境
  2. 在编译和测试之后生成文档
  3. 把文档归档为可追溯构建产物
  4. 把 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 生成并发布 apidoc 的流程图.svg

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,这时常见做法有两类:

  1. 使用已预装 Node.js 的构建节点
  2. 使用 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 这一组合中观察,它的价值可以概括为三点:

  1. 适合把接口说明当作构建产物来交付
  2. 适合在不暴露运行时文档接口的前提下生成静态 HTML
  3. 上限取决于团队是否真的把注释规范纳入日常开发与评审流程

一套较稳妥的落地方式通常包括:

  • Controller 或共享契约层维护 apidoc 注释
  • 通过 package.json 固定 docs:build 命令
  • Jenkinsfile 统一执行 mvn test + npm ci + apidoc
  • 通过 archiveArtifacts + publishHTML 完成留档与展示

如果只需要短期生成一个可浏览页面,这套方案并不复杂;但要让它长期可维护,重点始终不在“安装了 apidoc”,而在于以下三点是否持续执行:

  • 接口变更是否必须同步更新注释
  • 公共文档片段是否被统一抽象
  • 每次 Jenkins 构建是否真实地产出并发布文档

做到这三点,apidoc 才会从一个文档生成命令,演进为 Spring Boot 项目交付链路中的稳定组成部分。