使用 GitHub Actions 为 Java/Maven 项目配置 CI

1. 概述

持续集成(CI)帮助你在推送代码时立即发现错误,而不是等到合并时才“搞砸了”。本文将指导你为 Java/Maven 项目在 GitHub Actions 上构建 CI 流程:构建、运行测试、缓存依赖、使用 build matrix 测试多个 JDK 版本以及发布报告。读完后,你可以将工作流文件复制到实际项目中,几分钟内就能运行。

2. 准备示例项目

假设你有一个简单的 Spring Boot 项目,使用标准 Maven 结构。最少的 pom.xml 文件如下:

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.nonladev</groupId>
    <artifactId>order-service</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.25.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version>
            </plugin>
        </plugins>
    </build>
</project>

依赖项 junit-jupiter 和 assertj-core 用于编写测试。插件 maven-surefire-plugin 负责在调用 mvn test 时运行测试。

一个小的业务类用于测试:

package com.nonladev.order;

public class OrderCalculator {

    public double calculateTotal(double unitPrice, int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("数量必须大于 0");
        }
        return unitPrice * quantity;
    }
}

相应的测试:

package com.nonladev.order;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class OrderCalculatorTest {

    private final OrderCalculator calculator = new OrderCalculator();

    @Test
    void shouldCalculateTotalCorrectly() {
        double total = calculator.calculateTotal(50_000, 3);

        assertThat(total).isEqualTo(150_000);
    }

    @Test
    void shouldThrowWhenQuantityIsInvalid() {
        assertThatThrownBy(() -> calculator.calculateTotal(50_000, 0))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("数量必须大于 0");
    }
}

shouldCalculateTotalCorrectly 验证求和是否正确。shouldThrowWhenQuantityIsInvalid 验证当 quantity 无效时是否抛出异常。这是 CI 每次你推送代码时都会运行的测试。

3. 基础 CI 工作流

GitHub Actions 读取 .github/workflows/ 目录中的 YAML 文件。创建文件 .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Build and run tests
        run: mvn -B clean verify

逐部分解释:

  • on: 工作流在 push 或 pull_request 针对 main 分支时运行。
  • actions/checkout@v4: 将源代码克隆到运行器中。
  • actions/setup-java@v4: 安装 Temurin 版本的 JDK 21 — 在调用 mvn 之前总是需要这一步。
  • mvn -B clean verify: 标志 -B(批处理模式)禁用交互式输出,适合 CI 环境。clean verify 从头构建并运行测试(verify 阶段包括 test)。

推送提交,进入 GitHub 上的 Actions 标签,你会看到任务运行和类似的日志:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.nonladev.order.OrderCalculatorTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

如果测试失败,任务会显示红色状态,你会在日志中立即看到堆栈跟踪 — 无需 SSH 到任何机器。

4. 缓存 Maven 依赖以加快构建速度

每次任务在新的运行器(临时性)上运行时,Maven 都必须从 .m2/repository 重新下载所有依赖项。对于 Spring Boot 项目,这每次可能需要 1-2 分钟。setup-java 内置支持缓存:

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
          cache: 'maven'

属性 cache: 'maven' 自动基于 pom.xml 文件的哈希值缓存 ~/.m2/repository 目录。下次构建时,如果 pom.xml 没有更改,Actions 会立即恢复缓存,跳过重新下载每个 jar 的步骤。

你可以通过查看添加缓存前后“构建和运行测试”步骤的时间来检查效果 — 通常减少 30-60%。

注意: 如果项目使用 Maven Wrapper(./mvnw),应该根据 mvnw 进行缓存,而不是依赖运行器上预装的 Maven 版本,因为 GitHub 每次更新镜像时,默认 Maven 版本可能会改变。

5. 使用 build matrix 测试多个 JDK 版本

如果你维护需要兼容多个 Java 版本的库,使用 strategy.matrix 并行运行多个任务:

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        java-version: [ '17', '21' ]

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK ${{ matrix.java-version }}
        uses: actions/setup-java@v4
        with:
          java-version: ${{ matrix.java-version }}
          distribution: 'temurin'
          cache: 'maven'

      - name: Build and run tests
        run: mvn -B clean verify

matrix.java-version 列出要测试的版本列表。GitHub Actions 自动创建 2 个并行运行的任务:一个使用 JDK 17,一个使用 JDK 21。变量 ${{ matrix.java-version }} 被替换到每个相应的任务中。

Actions 标签上的结果显示 2 个单独的任务,例如 build (17) 和 build (21),帮助你立即了解项目在哪个 Java 版本失败。

6. 发布测试报告和检查覆盖率

默认情况下,测试日志只在 Actions 中显示为文本形式。为了获得更容易阅读的报告,使用 dorny/test-reporter action:

      - name: Build and run tests
        run: mvn -B clean verify

      - name: Publish test report
        uses: dorny/test-reporter@v1
        if: success() || failure()
        with:
          name: Maven Tests
          path: '**/surefire-reports/*.xml'
          reporter: java-junit

if: success() || failure():此步骤在测试失败时也会运行(但在任务被取消时不会运行),以便你始终看到报告。path 指向 Surefire 在运行测试后生成的 XML 文件。报告以 PR 中的单独检查形式出现,列出每个测试用例的通过/失败情况。

如果要测量覆盖率,请将 JaCoCo 插件添加到 pom.xml:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.12</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

运行 mvn verify 会自动在 target/site/jacoco/index.html 生成 HTML 报告。你可以将其上传为工件:

      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: jacoco-report
          path: target/site/jacoco/

此工件可以从工作流运行摘要页面下载,可以直接在浏览器中查看。

7. 构建状态徽章

工作流运行稳定后,将徽章添加到 README.md 中,以便人们无需进入 Actions 就能了解 CI 状态:

![CI](https://github.com/<org>/<repo>/actions/workflows/ci.yml/badge.svg)

用实际的 GitHub 名称替换 <org>/<repo>。徽章根据默认分支上最近一次运行的结果自动更新为绿色/红色。

8. 常见错误

  • 忘记设置 java-version 与 maven.compiler.source 匹配:本地构建通过但 CI 失败,错误为 release version X not supported。始终在 pom.xml 和工作流之间同步 JDK 版本。
  • 使用 mvn test 而不是 mvn verify:如果项目在 verify 阶段有集成测试运行(例如 Failsafe 插件),mvn test 会跳过它们,造成虚假的安全感。
  • 更改依赖项时缓存密钥错误:如果你自己编写缓存操作(actions/cache)但忘记将 pom.xml 包含在密钥哈希中,旧缓存可能导致构建使用过时的依赖项。使用 setup-java 的 cache: 'maven' 来避免这种情况。
  • 在没有 Docker/网络稳定的自托管运行器上运行 CI:Maven 需要从中央存储库下载依赖项;如果运行器被网络阻止,构建会在下载步骤卡住。检查代理或使用内部 Maven 镜像。

注意: 如果你的项目很小,只有几个简单的测试,并且不经常部署,一个包含 matrix 和覆盖报告的完整 CI 工作流可能会过度设计。从第 3 节的基本工作流开始,然后随着项目增长逐步扩展。

9. 结论

只需几十行 YAML,你就有了一个自动构建、测试、缓存依赖项和发布报告的 Maven 项目 CI 流程。从简单的工作流开始,然后根据实际需求添加 JDK matrix、JaCoCo 覆盖率和 README 徽章。最重要的不是工作流的复杂性,而是 CI 在你每次推送代码时都能稳定可靠地运行。

github-actionsjavamavenci-cdtesting