从手动维护到自动化:某团队通过Jenkins流水线集成Swagger实现API文档随代码实时更新避免文档与代码脱节
文档与代码的”离婚危机”
先说说我们团队曾经经历过的一段”黑暗时期”——那是2023年初,我们组接到了一个跨部门的大型项目,涉及到前端、移动端和第三方对接。项目负责人老王在第一次周会上就吐槽了一句:”咱们这API文档,怕不是上个月的吧?”
当时的情况是这样的:后端同学改完代码,心里想着”顺手把文档也更新一下”,结果忙起来就忘了;前端同学调接口的时候发现文档写的参数和实际返回的不一样,沟通成本直线上升;更头疼的是,每次上线前都要花半天时间核对文档,文档和代码就像两个不相干的平行世界。
有一次线上出bug,排查了两个小时,最后发现是文档误导——文档里说某个字段是String类型,实际代码里返回的是Integer。这种”文档欺诈”的事情,在当时几乎每周都会发生。
痛苦的觉醒
转折点发生在某个周五的下午。我们接到了一个紧急需求,需要在周一之前完成三个核心接口的改动,同时还要更新对应的文档。结果周五到周日,整个团队都在加班填坑——不是因为代码难写,而是因为要一遍遍地同步文档。
当时团队里的Java后端开发小李,在周末的微信群里发了一句话:”要是文档能自动跟代码走,该多好啊。”
这句话像一颗种子,在周一的晨会上被我们种进了实际执行计划里。
调研:为什么是Swagger + Jenkins?
我们一开始也尝试过其他方式。比如让产品经理手动维护Confluence上的文档,结果还是脱节;也有同学建议用Postman Collection,但导出和导入的流程依然繁琐。
经过几轮讨论和调研,我们最终锁定了Swagger + Jenkins流水线这个方案,原因很简单:
- Swagger是目前最成熟的API文档工具,支持自动生成文档,和Spring Boot生态集成无缝
- Jenkins是我们团队已经在用的CI/CD工具,学习成本低
- 两者结合可以实现”代码提交即更新文档”,完全自动化
技术选型:详细方案落地
第一步:在项目中集成Swagger
我们用的是Spring Boot项目,所以引入了springdoc-openapi这个库(比老的springfox更现代,社区维护更活跃)。
首先在pom.xml中添加依赖:
<!-- SpringDoc OpenAPI (Swagger) 依赖 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.7.0</version>
</dependency>
<!-- 用于生成OpenAPI JSON/YAML -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
<version>1.7.0</version>
</dependency>
然后在配置类中定义Swagger的基本信息:
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("用户服务API文档")
.version("1.0")
.description("本服务负责用户相关的所有接口,包含用户注册、登录、信息修改等功能")
.contact(new Contact()
.name("后端开发组")
.email("backend@company.com"))
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0")))
.externalDocs(new ExternalDocumentation()
.description("接口文档在线版")
.url("https://api-docs.internal/user-service"));
}
}
接着在每个Controller上添加注解,这是最关键的一步——文档质量取决于代码注释的质量:
@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "用户管理", description = "用户相关的增删改查操作")
public class UserController {
@Operation(
summary = "根据ID查询用户信息",
description = "返回用户的详细信息,包含ID、用户名、邮箱、注册时间等",
responses = {
@ApiResponse(responseCode = "200", description = "查询成功",
content = @Content(schema = @Schema(implementation = UserDTO.class))),
@ApiResponse(responseCode = "404", description = "用户不存在")
}
)
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(
@Parameter(description = "用户ID,必须为正整数", required = true)
@PathVariable Long id
) {
// 业务逻辑...
}
@Operation(
summary = "创建新用户",
description = "创建新用户账号,需要填写用户名、密码和邮箱。密码长度需在6-20位之间。",
requestBody = @RequestBody(
description = "用户创建请求体",
required = true,
content = @Content(schema = @Schema(implementation = CreateUserRequest.class))
),
responses = {
@ApiResponse(responseCode = "201", description = "创建成功"),
@ApiResponse(responseCode = "400", description = "参数校验失败")
}
)
@PostMapping
public ResponseEntity<String> createUser(
@Valid @RequestBody CreateUserRequest request
) {
// 业务逻辑...
}
}
到这里,本地启动项目后访问http://localhost:8080/swagger-ui.html,就能看到自动生成的文档了。
第二步:配置Jenkins流水线
我们用的Jenkins版本是2.387.1,流水线用声明式语法(Declarative Pipeline)来写,可读性更好。
首先是Jenkinsfile的编写:
pipeline {
agent any
environment {
// 项目基本信息
APP_NAME = "user-service"
DOCKER_IMAGE = "registry.internal/user-service"
SWAGGER_OUTPUT_DIR = "${workspace}/target/generated-resources/openapi"
DOCS_REPO_URL = "git@gitlab.internal:api-docs/user-service-docs.git"
DOCS_BRANCH = "main"
}
stages {
stage('代码拉取') {
steps {
checkout([
$class: 'GitSCM',
branches: [[name: env.BRANCH_NAME ?: 'main']],
userRemoteConfigs: [[
url: 'git@gitlab.internal:backend/user-service.git',
credentialsId: 'gitlab-ssh-credential'
]]
])
}
}
stage('Maven构建') {
steps {
sh '''
mvn clean package -DskipTests -Popenapi
echo "构建完成,开始生成OpenAPI规范"
'''
}
}
stage('生成API文档') {
steps {
sh '''
# 执行Swagger代码生成,输出JSON格式
java -jar tools/swagger-generator.jar \
--input target/classes \
--output ${SWAGGER_OUTPUT_DIR}/openapi.json \
--format json
# 转换为HTML格式,方便直接查看
swagger-cli bundle ${SWAGGER_OUTPUT_DIR}/openapi.json \
--type json \
--outfile ${SWAGGER_OUTPUT_DIR}/openapi-bundled.json
# 生成静态HTML文档
npx @redocly/cli build-docs \
${SWAGGER_OUTPUT_DIR}/openapi.json \
--output ${SWAGGER_OUTPUT_DIR}/index.html
'''
}
}
stage('部署文档到Git') {
steps {
script {
sh '''
cd ${SWAGGER_OUTPUT_DIR}
git init
git config user.email "jenkins@company.com"
git config user.name "Jenkins CI"
git add .
git commit -m "docs: 自动更新API文档 - ${BUILD_NUMBER}"
git remote add origin ${DOCS_REPO_URL}
git push -f origin main
'''
}
}
}
stage('部署应用') {
when {
branch 'main'
}
steps {
sh '''
docker build -t ${DOCKER_IMAGE}:${BUILD_NUMBER} .
docker push ${DOCKER_IMAGE}:${BUILD_NUMBER}
# 触发K8s滚动更新
kubectl set image deployment/user-service \
user-service=${DOCKER_IMAGE}:${BUILD_NUMBER}
'''
}
}
}
post {
success {
mail body: """
构建成功!
项目: ${APP_NAME}
构建号: ${BUILD_NUMBER}
文档地址: https://docs.internal/user-service/${BUILD_NUMBER}/
Git仓库: ${DOCS_REPO_URL}
""",
from: "jenkins@company.com",
subject: "✅ [${APP_NAME}] 构建成功 #${BUILD_NUMBER}",
to: "team@company.com"
}
failure {
mail body: """
构建失败,请检查日志!
项目: ${APP_NAME}
构建号: ${BUILD_NUMBER}
失败原因: ${currentBuild.result}
""",
from: "jenkins@company.com",
subject: "❌ [${APP_NAME}] 构建失败 #${BUILD_NUMBER}",
to: "team@company.com"
}
}
}
第三步:配置Maven插件自动生成OpenAPI
为了让构建过程更顺畅,我们在Maven层面也做了配置:
<!-- pom.xml 中的插件配置 -->
<build>
<plugins>
<!-- SpringDoc插件:构建时生成openapi.json -->
<plugin>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-maven-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>generate-openapi</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<apiDocsUrl>http://localhost:8080/v3/api-docs</apiDocsUrl>
<outputFileName>openapi.json</outputFileName>
<outputDir>${project.build.directory}/generated-resources/openapi</outputDir>
</configuration>
</plugin>
<!-- 确保构建时跳过测试,只关注文档生成 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
实施过程中遇到的坑
说回实际操作,这条路并不是一帆风顺的。我们踩了几个典型的坑:
坑一:构建环境缺少JavaFX
我们用的Jenkins节点是Linux服务器,默认安装的是headless版JDK。运行Swagger UI的时候报了java.lang.reflect.InaccessibleObjectException——因为SpringDoc依赖JavaFX来做某些渲染。
解决方案是在Jenkinsfile中添加环境变量:
environment {
JAVA_TOOL_OPTIONS = "-Djdk.attach.allowAttachSelf=true --add-opens java.base/java.lang=ALL-UNNAMED"
}
坑二:文档仓库的推送冲突
一开始我们直接在流水线里git push,结果有一次因为网络抖动,推送失败但Jenkins显示成功,导致文档版本和代码版本对不上。
后来我们改成了原子提交的方式——先暂存所有改动,构建成功后再一次性推送:
stage('生成并推送文档') {
steps {
script {
sh '''
mkdir -p ${SWAGGER_OUTPUT_DIR}
# 先生成文档
mvn springdoc-openapi:generate -Popenapi
# 打包到临时目录
tar -czf /tmp/docs-${BUILD_NUMBER}.tar.gz \
-C ${SWAGGER_OUTPUT_DIR} .
'''
// 只有上面成功了才执行下面的
sh '''
cd ${SWAGGER_OUTPUT_DIR}
git init
git config user.email "jenkins@company.com"
git config user.name "Jenkins CI"
git add .
git commit -m "docs: 自动更新API文档 - ${BUILD_NUMBER}"
git remote add origin ${DOCS_REPO_URL}
git push -f origin main
'''
}
}
}
坑三:多模块项目的路径问题
我们公司项目是多模块结构,有user-service、order-service、payment-service等多个子模块。SpringDoc默认只扫描主模块的注解,其他模块的接口文档出不来。
解决办法是在每个模块的pom.xml中都添加SpringDoc依赖,并在主模块的配置中指定扫描路径:
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("用户服务API文档")
.version("1.0")
.description("本服务负责用户相关的所有接口"))
// 指定扫描所有模块的API
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
.components(new Components()
.addSecuritySchemes("Bearer Authentication",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
同时在application.yml中配置:
springdoc:
api-docs:
path: /v3/api-docs
enabled: true
swagger-ui:
path: /swagger-ui.html
enabled: true
# 扫描所有模块
packages-to-scan:
- com.company.user.controller
- com.company.user.infrastructure.controller
# 同时包含其他模块
paths-to-match:
- /api/**
上线后的实际效果
实施完成后,我们把流水线跑了一遍。第一次看到构建成功的邮件时,整个团队都松了一口气。
现在的情况是:
- 每次代码提交到
main分支,Jenkins自动触发流水线 - 构建成功后,API文档自动更新并推送到文档仓库
- 前端和测试同学可以直接访问
https://docs.internal/user-service/查看最新文档 - 再也不用担心文档和代码不一致的问题了
老王在第二次周会上说了一句特别实在的话:”以前每周五下午花两小时对文档,现在这部分时间我们可以用来做别的事。”
后续优化方向
当然,事情不是一劳永逸的。我们后续还在做几件事:
- 文档版本管理:不同版本的API文档需要保留历史版本,方便回溯
- 接口变更通知:当接口有重大变更时,自动通知相关的前端和测试同学
- 测试覆盖率关联:把API测试的覆盖率也接入到流水线中,形成闭环
这些优化我们计划在下个季度逐步落地。如果你也正在被”文档与代码脱节”这个问题困扰,希望我们的经验能给你一些参考。有问题随时评论区交流,咱们一起把后端开发体验搞得更好。
