广告位 · 页头(后台-模块管理-新建模块后替换此区域)

Spring Boot项目如何借助GitHub Actions实现CI/CD自动化部署?

把Spring Boot应用从代码提交到线上运行,中间要经历编译、测试、打包、上传、重启等多个步骤。手动执行这些操作并不复杂,但每次改动都重复一遍,容易漏掉测试或忘记同步环境配置。GitHub Actions作为GitHub自带的自动化平台,可以用YAML文件描述这些步骤,并在push、pull request或定时任务触发时自动执行。本文会以Maven构建的单体Spring Boot项目为例,演示如何搭建一条包含自动测试、打包、上传制品、部署到Linux服务器的流水线,并说明配置过程中容易忽略的缓存、权限和多环境问题。

Spring Boot项目如何借助GitHub Actions实现CI/CD自动化部署?

一、先理解GitHub Actions的工作单元

GitHub Actions的配置存放在项目根目录的.github/workflows目录下,每个YAML文件代表一条工作流。工作流由name、on、jobs等顶级字段组成。on用于定义触发条件,push表示推送到任意分支时执行,pull_request表示创建或更新PR时执行,workflow_dispatch允许手动触发,schedule支持cron表达式定时执行。对Spring Boot项目而言,最常见的做法是push到main分支触发完整构建和部署,push到dev分支只跑测试,tag推送时触发发布流程。触发条件写得越精确,越能减少无效运行次数。

一个job会分配在一台独立的虚拟机上执行,默认使用GitHub托管的runner,常见系统包括ubuntu-latest、windows-latest和macos-latest。Spring Boot后端服务通常选择ubuntu-latest,因为Linux环境与生产服务器更接近,启动速度快,软件源也齐全。job内部由多个step顺序执行,每个step可以运行shell命令,也可以使用别人发布到GitHub Marketplace的action,例如checkout、setup-java、upload-artifact等。多个job默认并行执行,如果需要先构建后部署,可以用needs字段声明依赖顺序。

下面是一个最基础的Spring Boot工作流,它只做代码检出、JDK安装和Maven打包。这个结构后续可以继续扩展测试、缓存和部署任务。

name: spring-boot-ci
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17
      - name: Build with Maven
        run: mvn -B package --file pom.xml

二、为Spring Boot项目配置完整的构建与测试流水线

只执行package并不算真正的CI,完整的持续集成应当在每次提交后自动运行单元测试和集成测试。Spring Boot项目通常依赖数据库、Redis或消息队列,如果测试代码里用到了这些中间件,GitHub Actions runner上并不会天然提供。可以在job里使用services字段启动临时容器,例如MySQL、PostgreSQL或Redis,测试结束后这些容器会自动销毁,不需要额外清理。

依赖缓存是另一个影响构建速度的关键点。Maven和Gradle下载依赖通常要占用几十秒甚至几分钟,如果每次构建都重新下载,会拖慢整个流水线。setup-java这个action从v4版本开始支持cache参数,传maven或gradle后会自动缓存本地仓库目录。缓存命中后,后续构建可以直接复用依赖,通常能节省一半以上的时间。需要注意的是,当项目里的pom.xml或build.gradle发生变化时,缓存键也会随之变化,从而触发新的依赖解析。

下面的工作流加入了MySQL服务容器、完整测试命令以及制品上传步骤。测试阶段使用mvn verify而不是mvn test,因为verify会执行到package之前的完整生命周期,能发现更多集成问题。上传制品使用actions/upload-artifact,方便在Actions页面下载构建好的Jar包,也便于后续部署job直接获取产物。

name: spring-boot-build
on:
  push:
    branches: [ main ]
  pull_request:
jobs:
  test-and-package:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: app_db
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17
          cache: maven
      - name: Run unit and integration tests
        run: mvn -B verify --file pom.xml
        env:
          SPRING_DATASOURCE_URL: jdbc:mysql://127.0.0.1:3306/app_db
          SPRING_DATASOURCE_USERNAME: root
          SPRING_DATASOURCE_PASSWORD: root
      - name: Upload jar artifact
        uses: actions/upload-artifact@v4
        with:
          name: app-jar
          path: target/*.jar

这里还有两个容易踩坑的地方。第一是JDK版本必须与项目使用的版本一致,例如本地开发用Java 17,CI里也应当指定java-version为17,否则可能出现编译错误或UnsupportedClassVersionError。第二是Maven wrapper问题,如果团队习惯用mvnw而不是系统Maven,应当把run命令改成./mvnw,并确保mvnw文件在仓库中且有可执行权限,不然Linux runner会报Permission denied。

三、将Jar部署到Linux服务器并用systemd托管

构建完成只是第一步,如果希望每次合并到main分支后代码能自动上线,还需要部署环节。对于单体Spring Boot服务,比较直接的方式是通过SSH把Jar包上传到Linux服务器,然后重启对应的systemd服务。GitHub Actions生态中有现成的scp-action和ssh-action可以完成上传和远程命令执行,服务器地址、用户名、私钥等信息通过GitHub Secrets保存,不能写进YAML文件明文提交。

在配置部署前,需要在仓库的Settings里新建Secrets,例如SSH_HOST、SSH_USER、SSH_KEY、SSH_PORT。私钥最好使用单独生成的部署专用密钥,并限制该密钥只能访问目标服务器的指定账户。部署job通过needs字段依赖前面的test-and-package,只有测试和打包通过后才会执行。同时可以用if条件限定只有main分支的push才允许部署,避免开发分支触发无意义的线上更新。

下面是一个完整的部署job示例,它先下载之前上传的Jar制品,再通过SCP上传到服务器,最后用SSH重启systemd服务并查看状态。

  deploy:
    needs: test-and-package
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: app-jar
          path: target
      - name: Deploy jar to server
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.SSH_PORT }}
          source: "target/*.jar"
          target: "/opt/app/"
      - name: Restart service
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.SSH_PORT }}
          script: |
            sudo systemctl restart spring-boot-app
            sudo systemctl status spring-boot-app --no-pager

服务器上的Spring Boot进程不建议直接用nohup或screen挂后台,因为服务器重启后进程不会自动拉起,崩溃后也不会恢复。systemd是Linux下更可靠的服务管理方式。下面是一个典型的systemd unit文件,放在/etc/systemd/system/spring-boot-app.service,通过systemctl enable设置开机自启。

[Unit]
Description=Spring Boot Application
After=network.target
[Service]
User=appuser
WorkingDirectory=/opt/app
ExecStart=/usr/bin/java -jar /opt/app/app.jar --spring.profiles.active=prod
SuccessExitStatus=143
Restart=always
RestartSec=10
EnvironmentFile=/etc/app.env
[Install]
WantedBy=multi-user.target

SuccessExitStatus=143这个配置很关键,Java进程收到SIGTERM信号时的退出码是143,如果不在systemd里声明为成功状态,systemd会认为服务异常退出并记录Failure。Restart=always则能在进程意外崩溃后自动拉起。部署脚本中应当优先使用systemctl restart而不是kill -9,让Spring Boot应用有足够时间完成优雅关闭,例如等待正在处理的HTTP请求结束、释放数据库连接池等。

四、多环境、容器化与发布策略的进阶思路

真实的项目通常不只一个环境,开发、测试、预发布、生产各自需要独立的配置和部署目标。GitHub Actions支持通过environment字段声明部署环境,每个环境可以配置独立的Secrets,也可以设置审批规则。例如main分支部署到production环境时,需要指定人员点击批准后才继续执行,这给发布增加了一道人工确认的安全门槛。develop分支则可以自动部署到staging环境,不需要审批。

如果团队已经使用Docker或Kubernetes,还可以进一步把Jar包封装成镜像。Dockerfile采用多阶段构建,第一阶段用Maven镜像编译打包,第二阶段只保留JRE运行环境,这样最终镜像体积更小,也更安全。构建完成后用docker/build-push-action推送到GitHub Container Registry或其他镜像仓库,部署时直接更新容器即可。相比之下,传统Jar部署更简单直接,适合小型项目;容器化部署环境一致性强,更适合微服务和集群场景。

下面的Dockerfile给出了一个多阶段构建的示例,它先生成可执行Jar,再复制到精简的JRE镜像中运行。

FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /workspace
COPY pom.xml .
RUN mvn -B dependency:go-offline
COPY src ./src
RUN mvn -B package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /workspace/target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

多环境部署时,可以用strategy.matrix对不同profile分别打包,也可以构建同一个镜像,通过环境变量在容器启动时切换配置。无论选择哪种方式,都应该在流水线中增加基本的健康检查,例如部署后请求/actuator/health接口,确认服务真正可用后再结束任务。如果健康检查失败,可以回滚到上一个版本,或者通过通知插件把失败信息发送到团队沟通群。CI/CD的目标不是把手工操作替换成脚本,而是让每次交付都更快、更稳、更可追溯,围绕这个目标持续优化流水线,才能真正发挥自动化部署的价值。

广告位 · 文章底部(后台-模块管理替换此区域)