GitLab CI/CD 实战:搞懂 Java Maven 项目的构建流程
在 Java 项目中,我们经常会使用 GitLab CI/CD 自动完成代码编译、打包和部署。
很多人第一次看到 .gitlab-ci.yml 时,会发现配置只有几十行,但里面同时出现了 rules、stage、image、tags、cache、script、artifacts 等概念,很容易混淆。
本文以一段实际的 Maven 项目构建配置为例,从一次 Job 是如何被触发、如何选择 Runner、如何使用 Docker 环境构建,到 Maven 依赖缓存和 Artifact 保存,把整个构建流程串起来。
1. 先看完整配置
job_build:
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"'
stage: build
image: maven:3.8.8-eclipse-temurin-21
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
tags:
- build
cache:
key: "${CI_PROJECT_NAME}-maven"
paths:
- .m2/repository/
policy: pull-push
script:
- echo "Building the project"
- mvn -B -ntp -pl app-service -am -DskipTests clean package
- mkdir -p dist
- cp app-service/target/app.jar dist/app.jar
- test -s dist/app.jar
artifacts:
name: "build-${CI_COMMIT_SHORT_SHA}"
paths:
- dist/app.jar
expire_in: 7 days这段配置整体可以理解为:
当代码 push 到
dev分支时,GitLab 找到带有build标签的 Runner,使用 Maven 3.8.8 + JDK 21 的环境构建app-service模块,将生成的app.jar保存为 Artifact 7 天,同时缓存 Maven 依赖,提高下一次构建速度。
接下来把这段配置拆开来看。
2. GitLab CI 是怎么运行这个 Job 的
Job 名称
job_build:job_build 是 Job 名称,本质上只是 GitLab 用来标识任务的名称,可以自定义。
例如一个流水线可以有多个 Job:
stages:
- build
- test
- deploy
job_build:
stage: build
job_test:
stage: test
job_deploy:
stage: deployJob 名称也完全可以写成:
build_backend:或者:
maven_build:只要名称不冲突即可。
rules:什么时候执行
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"'rules 用来决定当前 Job 在什么情况下执行。
这里包含两个条件:
CI_PIPELINE_SOURCE == push表示流水线来源是代码 push。
CI_COMMIT_BRANCH == dev表示当前分支是 dev。
两个条件通过 && 连接,所以必须同时满足:
代码发生 push
并且
push 的分支是 dev例如:
git push origin dev会满足条件。
而:
git push origin master不会执行当前 Job。
如果流水线是 Merge Request 触发的:
CI_PIPELINE_SOURCE = merge_request_event同样不满足这里的规则。
可以简单记住:
rules → 决定 Job 要不要执行stage:Job 属于哪个阶段
stage: build表示当前 Job 属于 build 阶段。
通常 .gitlab-ci.yml 顶部会定义:
stages:
- build
- test
- deploy默认情况下,流水线会按照阶段顺序执行:
build
↓
test
↓
deploy也就是说,前一个阶段成功完成后,才会继续进入下一个阶段。
所以:
stage: build可以理解为:
job_build属于构建阶段。
tags:由哪个 Runner 执行
tags:
- buildtags 用来匹配 GitLab Runner。
假设有两个 Runner:
Runner A
标签:buildRunner B
标签:deploy当前 Job 配置:
tags:
- buildGitLab 就会寻找能够匹配 build 标签的 Runner 来执行这个 Job。
这里特别容易和 stage 混淆:
stage → 决定 Job 属于哪个流水线阶段
tags → 决定哪个 Runner 执行 Job它们的名字完全不需要相同,例如:
stage: build
tags:
- ubuntu-runner也是完全正常的。
image:使用什么构建环境
image: maven:3.8.8-eclipse-temurin-21表示当前 Job 使用:
Maven 3.8.8
JDK 21对应的 Docker 镜像作为运行环境。
因此在 Job 中可以直接执行:
mvn clean package而不需要再手动安装 Maven 和 Java。
如果 Runner 使用 Docker Executor,过程大致如下:
GitLab Runner
↓
准备 Maven Docker 镜像
↓
启动 Job 容器
↓
检出项目代码
↓
执行 script
↓
Job 结束
↓
容器被删除需要注意:
image: maven:3.8.8-eclipse-temurin-21表示“使用这个镜像作为 Job 的运行环境”,并不是每次都重新构建这个 Maven 镜像。
如果使用 Docker Executor,Runner 是否重新拉取镜像由 pull_policy 决定:
always(默认)
↓
每次 Job 都会尝试拉取镜像
if-not-present
↓
本地不存在镜像时才拉取
never
↓
不拉取,只使用 Runner 本地已有镜像因此,“Runner 本地已有镜像就一定直接使用”这个理解并不准确,具体要看 Runner 的镜像拉取策略。
注意:
image主要用于 Docker、Kubernetes 等基于镜像的 Executor。 如果 Runner 使用的是 Shell Executor,Job 会直接在 Runner 所在机器上执行,.gitlab-ci.yml中的image不会为 Job 创建 Maven Docker 容器。
3. Maven 依赖为什么要做 Cache
MAVEN_OPTS:修改 Maven 本地仓库
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"variables 用来定义 Job 中可以使用的环境变量。
这里设置了:
MAVEN_OPTS其中:
-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository用于修改 Maven 本地仓库的位置。
Maven 默认依赖目录通常是:
~/.m2/repository在容器里可能类似:
/root/.m2/repository问题在于,CI 中 Docker Job 容器通常是临时的。
Job 执行完成以后,容器可能会被删除,那么容器内部下载的 Maven 依赖也就无法直接给下一次 Job 使用。
下一次构建又要重新下载:
Spring Boot
MyBatis
MySQL Driver
Jackson
Lombok
...这样会明显拖慢构建速度。
所以这里把 Maven 仓库改到:
$CI_PROJECT_DIR/.m2/repository$CI_PROJECT_DIR 是 GitLab 内置变量,代表当前项目的工作目录。
例如:
/builds/example/demo-project那么 Maven 仓库就是:
/builds/example/demo-project/.m2/repository这样就方便交给 GitLab Cache 保存。
cache:保存 Maven 依赖
cache:
key: "${CI_PROJECT_NAME}-maven"
paths:
- .m2/repository/
policy: pull-pushcache 主要用于保存可以重复使用的数据,加快后续构建速度。
对于这个项目来说,缓存的就是:
.m2/repository/也就是 Maven 依赖。
前面的 MAVEN_OPTS 和这里的 cache 是配套使用的:
MAVEN_OPTS
↓
把 Maven 仓库改到项目目录
↓
.m2/repository
↓
GitLab Cache
↓
保存 Maven 依赖第一次构建和第二次构建有什么区别
第一次构建时没有缓存:
启动 Job 容器
↓
项目代码位于
/builds/example/demo-project
↓
恢复 Cache
↓
没有历史缓存
↓
执行 mvn clean package
↓
Maven 发现 .m2/repository 没有依赖
↓
从 Maven 中央仓库 / 公司私服下载
↓
保存到 .m2/repository
↓
使用这些 Jar 完成编译
↓
Job 结束
↓
Runner 保存 Cache第二次构建时:
启动新的 Job 容器
↓
Runner 恢复之前的 Cache
↓
.m2/repository 已经存在大量依赖
↓
执行 mvn clean package
↓
已有依赖直接使用
↓
只下载新增或缺失依赖
↓
构建速度明显提升cache.key:缓存标识
key: "${CI_PROJECT_NAME}-maven"key 可以理解为 Cache 的标识。使用相同 key 的 Job,会尝试复用同一份缓存。
假设:
CI_PROJECT_NAME = demo-project那么最终 key 就是:
demo-project-maven需要注意:不同 GitLab 项目的 Cache 本身就是隔离的,所以这里使用 CI_PROJECT_NAME 并不是为了防止不同项目之间串缓存。
cache.key 更重要的作用,是在同一个项目中区分不同 Job、分支或不同缓存策略。
例如按分支区分缓存:
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .m2/repository/可以理解为:
dev
→ dev 分支使用自己的 Cache
master
→ master 分支使用自己的 Cache如果希望进一步按 Job + 分支区分,也可以使用:
key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"具体使用哪种 key,要根据项目是否希望多个分支或多个 Job 共用 Maven 依赖缓存来决定。
cache.paths:缓存哪些目录
paths:
- .m2/repository/表示把项目目录中的:
.m2/repository/作为缓存内容保存。
policy:缓存策略
policy: pull-pushpull-push 表示:
Job 开始
↓
拉取已有 Cache
↓
执行构建
↓
Cache 内容可能发生变化
↓
Job 结束时重新保存 Cache常见策略包括:
pull-push
pull
push对于普通 Maven 构建,pull-push 是很常见的选择。
4. Maven 构建命令详解
真正执行构建的是 script:
script:
- echo "Building the project"
- mvn -B -ntp -pl app-service -am -DskipTests clean package
- mkdir -p dist
- cp app-service/target/app.jar dist/app.jar
- test -s dist/app.jarRunner 会从上到下依次执行这些 Shell 命令。
只要某个关键命令返回非 0:
exit code != 0当前 Job 默认就会失败。
Maven 核心构建命令
mvn -B -ntp -pl app-service -am -DskipTests clean package可以把它拆成以下几部分理解。
-B 和 -ntp:适配 CI 环境
-B等价于:
--batch-mode表示 Maven 使用批处理模式。
CI 属于无人值守环境,一般不应该等待人工输入,因此这种模式非常适合 GitLab CI、Jenkins 等自动化环境。
-ntp等价于:
--no-transfer-progress用于关闭依赖下载进度显示,减少 CI 日志中的大量下载进度信息。
所以这两个参数可以理解为:
-B → 更适合无人值守的 CI 环境
-ntp → 减少无意义的下载日志-pl 和 -am:控制多模块构建
当前项目类似:
demo-project
├── pom.xml
├── common-module
│ └── pom.xml
└── app-service
└── pom.xml父项目 pom.xml 中定义:
<modules>
<module>common-module</module>
<module>app-service</module>
</modules>如果直接执行:
mvn clean package可能会构建所有 Module。
现在只希望主要构建:
app-service所以使用:
-pl app-service-pl 是 --projects,可以理解为:
我要构建哪个模块。
但是如果 app-service 又依赖当前 Maven Reactor 中的其他模块,仅指定 -pl 可能还不够,因此又加上:
-am-am 是 --also-make,表示把目标模块依赖的项目内部模块一起构建。
例如:
app-service
↓
common执行:
mvn -pl app-service -am packageMaven 会先处理它需要的依赖模块,再构建 app-service。
可以简单记:
-pl → 我要构建谁
-am → 它依赖的项目模块也一起构建-DskipTests:跳过测试执行
-DskipTests表示 Maven 构建过程中不执行测试。
例如项目中有:
src/test/java使用 -DskipTests 后,测试不会真正执行。
需要注意:
-DskipTests通常表示:
测试不执行
但测试代码仍可能编译而:
-Dmaven.test.skip=true通常表示:
测试不执行
测试代码也不编译如果流水线中另外设计了专门的测试阶段,那么 Build 阶段使用 -DskipTests 是一种常见做法。
clean package:清理并打包
clean主要用于清理旧构建结果,例如删除:
target/避免旧 Jar、旧 class 等构建文件影响当前打包。
package则执行 Maven 生命周期直到 package 阶段。
对于 Spring Boot 项目,通常最终会生成:
target/xxx.jar当前项目生成的是:
app-service/target/app.jar所以整条命令:
mvn -B -ntp -pl app-service -am -DskipTests clean package可以理解成:
以适合 CI 的方式运行 Maven,只构建
app-service及其需要的项目内部模块,跳过测试执行,清理旧构建并重新打包。
Maven 参数速查
5. 构建出来的 Jar 是怎么保存的
Maven 打包成功以后,当前项目会生成:
app-service/target/app.jar接下来还有三条命令:
mkdir -p dist
cp app-service/target/app.jar dist/app.jar
test -s dist/app.jar创建统一的产物目录
mkdir -p dist表示创建:
dist目录。
由于 Runner 通常在:
$CI_PROJECT_DIR中执行脚本,所以最终目录类似:
/builds/example/demo-project/dist-p 的作用之一是目录已经存在时不会因为这一点报错。
复制 Jar
cp app-service/target/app.jar dist/app.jar把 Maven 模块中生成的:
app-service/target/app.jar复制到统一位置:
dist/app.jar这样后面的 Artifact 和部署阶段就不需要关心 Jar 原本在哪个 Maven Module 中生成。
检查 Jar 是否存在且非空
test -s dist/app.jar-s 用于检查文件是否存在并且大小大于 0。
如果 dist/app.jar 不存在或是空文件,这条命令会返回非 0,Job 就会失败。
这样可以避免构建过程看似成功,但实际上没有拿到有效 Jar 的情况继续进入后续部署流程。
artifacts:上传构建结果
artifacts:
name: "build-${CI_COMMIT_SHORT_SHA}"
paths:
- dist/app.jar
expire_in: 7 daysartifacts 用于把当前 Job 产生的文件保存到 GitLab。
这里保存的是:
dist/app.jar流程可以理解为:
Runner
↓
生成 dist/app.jar
↓
Artifact Upload
↓
GitLab 保存即使 Docker Job 容器已经被删除,这个 app.jar 仍然可以从 GitLab 获取,也可以供后续 Deploy Job 使用。
Artifact 名称
name: "build-${CI_COMMIT_SHORT_SHA}"其中:
CI_COMMIT_SHORT_SHA是 GitLab 内置变量,表示当前 Git Commit 的短 SHA。
例如完整 Commit:
1d92bc2f26ee3d38e808cdc910edc0ebe1b4d47d短 SHA 可能是:
1d92bc2f最终 Artifact 名称类似:
build-1d92bc2f这样可以快速判断某个构建产物对应哪一次 Git Commit。
Artifact 保存多久
expire_in: 7 days表示该 Artifact 保存 7 天。
6. Cache 和 Artifact 到底有什么区别
这是 GitLab CI/CD 中非常容易混淆的一组概念。
Cache:为了让下一次构建更快
当前配置:
cache:
paths:
- .m2/repository/保存的是 Maven 依赖。
Cache 的主要目的不是发布构建结果,而是:
减少重复下载
提高后续构建速度常见 Cache 内容包括:
Maven 依赖
npm 依赖
pnpm store
Gradle CacheCache 即使丢失,通常也不会导致项目无法构建,只是需要重新下载依赖,构建会更慢。
Artifact:保存本次 Job 的结果,可提供给下一阶段使用
当前配置:
artifacts:
paths:
- dist/app.jar保存的是当前这一次构建真正产生的结果。
常见 Artifact 包括:
Jar
War
前端 dist
测试报告
覆盖率报告这些文件通常用于:
下载
部署
后续 Job 使用可以简单记成:
Cache
↓
为了快
Artifact
↓
为了保存本次构建结果对于 Java Maven 项目:
.m2/repository
→ Cache
app.jar
→ Artifact整个数据流可以画成:
GitLab CI Job
│
┌────────────┴────────────┐
│ │
Cache Artifact
│ │
.m2/repository dist/app.jar
│ │
↓ ↓
下一次构建继续使用 后续部署 / 下载使用
│ │
↓ ↓
提高速度 保存构建结果7. 把整个 GitLab CI 构建流程串起来
理解了前面的配置以后,这个 Job 的完整执行过程就很清楚了:
开发者
↓
git push origin dev
↓
GitLab 创建 Pipeline
↓
rules 判断
↓
是否为 push + dev
↓
满足条件
↓
进入 build 阶段
↓
GitLab 根据 tags=build 找 Runner
↓
Runner 准备 Maven 3.8.8 + JDK 21 环境
↓
检出项目代码
↓
恢复 Maven Cache
↓
.m2/repository
↓
执行 Maven 构建
↓
mvn -B -ntp -pl app-service -am -DskipTests clean package
↓
生成
app-service/target/app.jar
↓
创建 dist 目录
↓
复制 Jar
↓
dist/app.jar
↓
test -s 检查 Jar
↓
上传 Artifact
↓
build-${CI_COMMIT_SHORT_SHA}
↓
GitLab 保存 7 天
↓
更新 Maven Cache从更高层看,Java 项目的 CI/CD 可以先记成:
Git Push
↓
GitLab Pipeline
↓
Runner
↓
Maven Build
↓
Jar
↓
Artifact
↓
Deploy核心配置速查
尤其注意三个容易混淆的概念:
stage ≠ tags
cache ≠ artifacts
image ≠ 每次重新构建镜像总结
一段几十行的 GitLab CI 配置,背后其实串起了很多 CI/CD 核心概念:
Pipeline
Job
Stage
Runner
Docker Image
Variables
Cache
Artifact
Maven Module真正理解这些概念以后,再看更完整的流水线:
build
↓
test
↓
Docker Build
↓
deploy就会容易很多。
对于 Java / Spring Boot 项目,可以先牢牢记住:
CI/CD 本质上就是把以前开发人员手工执行的编译、打包、检查和部署命令,按照固定规则交给 Runner 自动执行。
