0
0
0

GitLab CI/CD 实战:搞懂 Java Maven 项目的构建流程

2026-09-18
文章摘要
|

在 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: deploy

Job 名称也完全可以写成:

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:
  - build

tags 用来匹配 GitLab Runner。

假设有两个 Runner:

Runner A
标签:build
Runner B
标签:deploy

当前 Job 配置:

tags:
  - build

GitLab 就会寻找能够匹配 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-push

cache 主要用于保存可以重复使用的数据,加快后续构建速度。

对于这个项目来说,缓存的就是:

.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-push

pull-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.jar

Runner 会从上到下依次执行这些 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 package

Maven 会先处理它需要的依赖模块,再构建 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 参数速查

参数

作用

mvn

执行 Maven

-B

Batch Mode,适合 CI 环境

-ntp

不显示依赖下载进度

-pl app-service

指定主要构建 app-service 模块

-am

同时构建目标模块需要的项目内部模块

-DskipTests

不执行测试

clean

删除旧的 target 构建结果

package

编译并打包项目


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 days

artifacts 用于把当前 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 Cache

Cache 即使丢失,通常也不会导致项目无法构建,只是需要重新下载依赖,构建会更慢。

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

核心配置速查

配置

作用

rules

决定 Job 是否执行

stage

Job 属于哪个流水线阶段

image

Job 使用什么运行环境

variables

定义 Job 环境变量

tags

指定哪个 Runner 执行

cache

缓存依赖,加快后续构建

script

Runner 真正执行的命令

artifacts

保存当前构建产生的结果

expire_in

Artifact 保存多久

尤其注意三个容易混淆的概念:

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 自动执行。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论