Java CI/CD案例

wen java案例 1

本文目录导读:

Java CI/CD案例

  1. 项目架构概览
  2. 技术栈
  3. CI/CD 流水线阶段设计

下面是一个完整的Java项目CI/CD案例,涵盖环境搭建Pipeline编写自动化测试部署


项目架构概览

📦demo-java-ci-cd
 ┣ 📂src
 ┃ ┣ 📂main/java/com/example
 ┃ ┣ 📂test/java/com/example
 ┣ 📂scripts
 ┃ ┣ 📜deploy.sh
 ┣ 📜pom.xml
 ┣ 📜Dockerfile
 ┗ 📜.gitlab-ci.yml (或 Jenkinsfile / .github/workflows)

技术栈

工具 说明
JDK 11/17 编译运行环境
Maven 依赖管理和构建
JUnit 5 + Mockito 单元测试
SonarQube 代码质量扫描
Docker + Docker Compose 容器化
GitLab CI / Jenkins / GitHub Actions CI/CD 编排
Nginx / Kubernetes 部署

CI/CD 流水线阶段设计

[Java代码提交] → [代码静态分析] → [编译打包] → [单元测试] → [生成Docker镜像] → [推送到仓库] → [部署到测试/生产环境]

完整配置示例 (以GitHub Actions为例)

.github/workflows/java-ci-cd.yml

name: Java CI/CD Pipeline
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
jobs:
  build:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: test_db
        ports:
          - 3306:3306
    steps:
    - name: Checkout代码
      uses: actions/checkout@v3
    # 1️⃣ 设置JDK
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
        cache: maven
    # 2️⃣ Maven构建 + 单元测试
    - name: Build和测试
      run: |
        mvn clean compile
        mvn test
    # 3️⃣ 代码质量扫描
    - name: SonarQube静态扫描
      env:
        SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
      run: mvn sonar:sonar -Dsonar.token=$SONAR_TOKEN \
           -Dsonar.host.url=$SONAR_HOST_URL \
           -Dsonar.projectKey=demo-java
    # 4️⃣ 打包成Jar包
    - name: Package
      run: mvn package -DskipTests
    # 5️⃣ 登录Docker Hub
    - name: Login Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}
    # 6️⃣ 构建和推送Docker镜像
    - name: Build and Push Docker Image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: |
          ${{ secrets.DOCKER_USERNAME }}/demo-java:latest
          ${{ secrets.DOCKER_USERNAME }}/demo-java:${{ github.sha }}
  # 7️⃣ 部署到测试环境
  deploy-test:
    needs: build
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    steps:
    - name: Deploy到测试环境
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.TEST_HOST }}
        username: ${{ secrets.SSH_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          docker pull ${{ secrets.DOCKER_USERNAME }}/demo-java:latest
          cd /opt/demo
          docker-compose up -d
  # 8️⃣ 生产环境部署(需手动审批)
  deploy-prod:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
    - name: Deploy到生产环境
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.PROD_HOST }}
        username: ${{ secrets.SSH_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          docker pull ${{ secrets.DOCKER_USERNAME }}/demo-java:latest
          cd /opt/demo-prod
          docker-compose up -d

Dockerfile

# 多阶段构建
FROM maven:3.8.4-openjdk-17-slim AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
# 运行阶段
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
# 设置时区
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

docker-compose.yml

version: '3.8'
services:
  app:
    image: your_username/demo-java:latest
    container_name: demo-java-app
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=docker
      - DB_HOST=mysql
      - DB_PORT=3306
    depends_on:
      - mysql
    restart: always
  mysql:
    image: mysql:8.0
    container_name: demo-mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: demo_db
      MYSQL_USER: app_user
      MYSQL_PASSWORD: app_password
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
volumes:
  mysql_data:

pom.xml 配置(示例)

<properties>
    <java.version>17</java.version>
    <sonar.organization>your-org</sonar.organization>
    <jacoco.version>0.8.8</jacoco.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
        <!-- JaCoCo 覆盖率 -->
        <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>${jacoco.version}</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>
    </plugins>
</build>

测试类示例

@SpringBootTest
@ActiveProfiles("test")
class UserServiceTest {
    @Autowired
    private UserService userService;
    @MockBean
    private UserRepository userRepository;
    @Test
    void testCreateUser() {
        // 准备数据
        User user = new User();
        user.setName("张三");
        user.setEmail("zhangsan@test.com");
        // Mock行为
        when(userRepository.save(any(User.class))).thenReturn(user);
        // 调用测试
        User savedUser = userService.createUser(user);
        // 断言
        assertNotNull(savedUser);
        assertEquals("张三", savedUser.getName());
        // 验证交互
        verify(userRepository, times(1)).save(any(User.class));
    }
}

部署脚本 deploy.sh

#!/bin/bash
set -e
echo "🚀 ========== 开始部署 =========="
# 获取参数
ENV_NAME=$1
IMAGE_TAG=$2
# 拉取最新镜像
echo "📦 拉取镜像: ${IMAGE_TAG}"
docker pull your_username/demo-java:${IMAGE_TAG}
# 停止并删除旧容器
echo "🛑 停止旧容器..."
docker-compose -f docker-compose.${ENV_NAME}.yml down || true
# 启动新容器
echo "▶️ 启动新容器..."
docker-compose -f docker-compose.${ENV_NAME}.yml up -d
# 检查健康状态
echo "🏥 检查健康状态..."
sleep 30
curl -f http://localhost:8080/api/health || exit 1
echo "✅ ========== 部署成功 =========="

Jenkins Pipeline 版本(可选)

pipeline {
    agent any
    tools {
        jdk 'JDK17'
        maven 'MAVEN3'
    }
    environment {
        DOCKER_REGISTRY = 'registry.example.com'
        APP_NAME = 'demo-java'
        IMAGE_TAG = "${BUILD_NUMBER}"
    }
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        stage('静态代码分析') {
            steps {
                withSonarQubeEnv('sonarqube') {
                    sh 'mvn sonar:sonar -Dsonar.projectKey=demo-java'
                }
            }
        }
        stage('编译打包') {
            steps {
                sh 'mvn clean package'
            }
        }
        stage('构建Docker镜像') {
            steps {
                script {
                    sh """
                        docker build -t ${DOCKER_REGISTRY}/${APP_NAME}:${IMAGE_TAG} .
                        docker tag ${DOCKER_REGISTRY}/${APP_NAME}:${IMAGE_TAG} ${DOCKER_REGISTRY}/${APP_NAME}:latest
                    """
                }
            }
        }
        stage('推送到仓库') {
            steps {
                script {
                    withCredentials([usernamePassword(
                        credentialsId: 'dockerhub',
                        usernameVariable: 'USERNAME',
                        passwordVariable: 'PASSWORD'
                    )]) {
                        sh """
                            echo "${PASSWORD}" | docker login -u "${USERNAME}" --password-stdin
                            docker push ${DOCKER_REGISTRY}/${APP_NAME}:${IMAGE_TAG}
                            docker push ${DOCKER_REGISTRY}/${APP_NAME}:latest
                        """
                    }
                }
            }
        }
        stage('部署') {
            input {
                message "确认部署到生产环境?"
                ok "部署"
            }
            steps {
                sh """
                    ssh deploy-server "./deploy.sh prod ${IMAGE_TAG}"
                """
            }
        }
    }
    post {
        success {
            echo '构建成功!'
            // 发送通知
            slackSend channel: '#builds', message: "构建成功: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
        }
        failure {
            echo '构建失败!'
            slackSend channel: '#builds', message: "构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
        }
    }
}

十一、实际执行效果流程

[开发环境]
Git push → 编译 → 单元测试 → 代码扫描 → 生成镜像(dev tag) → 部署到dev环境
                                                      ↓
[测试环境]                                   合并到develop分支
Git push → 编译 → 集成测试 → 代码扫描 → 生成镜像(test tag) → 部署到test环境
                                                      ↓
[生产环境]                                   合并到main分支 + 审批
Git push → 编译 → 回归测试 → 安全扫描 → 生成镜像(prod tag) → 部署到生产

十二、最佳实践总结

  1. 分支策略

    • main → 生产
    • develop → 测试
    • feature/* → 功能开发
  2. 安全控制

    • 生产部署需要人工审批
    • 密钥使用Secret管理,不写入代码
    • 镜像签名验证
  3. 质量门槛

    • 代码覆盖率 ≥ 80%
    • SonarQube无Critical级别问题
    • 所有单元测试通过
  4. 部署策略

    • 蓝绿部署 / 滚动更新
    • 健康检查自动回滚
    • 日志集中收集
  5. 监控告警

    • Prometheus + Grafana监控
    • 告警通知接入钉钉/Slack
    • 分布式链路追踪 (Jaeger)

这个案例涵盖了完整的Java CI/CD流程,可根据实际项目需求灵活调整,遇到具体问题时,欢迎继续交流!

抱歉,评论功能暂时关闭!