Docker CI/CD实践

📌 本文由 12 篇相关文章智能合并整理而成

代码质量保障体系:从单元测试到 CI/CD

代码质量与工程化——从单元测试到 CI/CD 全链路

一、引言

代码质量是软件工程的永恒命题。良好的代码质量不仅意味着更少的 Bug,更意味着更低的维护成本、更快的交付速度和更顺畅的团队协作。然而,在追求交付速度的压力下,质量往往成为首当其冲的牺牲品。

工程化的核心思想是将软件开发从”手工作坊”升级为”工业流水线”——通过自动化工具、标准化流程和系统化的质量门禁,在保证效率的同时确保质量。本文将从单元测试出发,依次覆盖代码审查规范、Git 工作流和 CI/CD 流水线,最后介绍发布策略,帮助团队构建完整的质量保障体系。

二、单元测试

2.1 JUnit 5 核心特性

// JUnit 5 基础测试
@SpringBootTest
@AutoConfigureMockMvc
class OrderServiceTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private OrderRepository orderRepository;

    @InjectMocks
    @Autowired
    private OrderService orderService;

    private Order testOrder;

    @BeforeEach
    void setUp() {
        testOrder = new Order();
        testOrder.setId(1L);
        testOrder.setOrderNo("ORD20260516001");
        testOrder.setUserId(1001L);
        testOrder.setStatus("CREATED");
    }

    @Test
    @DisplayName("创建订单 - 成功场景")
    void testCreateOrder_Success() {
        when(orderRepository.save(any(Order.class)))
            .thenReturn(testOrder);

        Order result = orderService.createOrder(1001L, BigDecimal.valueOf(99.99));

        assertNotNull(result);
        assertEquals("ORD20260516001", result.getOrderNo());
        assertEquals("CREATED", result.getStatus());
        verify(orderRepository).save(any(Order.class));
    }

    @Test
    @DisplayName("取消订单 - 仅允许待支付状态")
    void testCancelOrder_OnlyPendingAllowed() {
        Order paidOrder = new Order();
        paidOrder.setId(2L);
        paidOrder.setStatus("PAID");
        when(orderRepository.findById(2L)).thenReturn(Optional.of(paidOrder));

        assertThrows(IllegalStateException.class, () -> {
            orderService.cancelOrder(2L);
        });
    }

    @ParameterizedTest
    @ValueSource(strings = {"CREATED", "PENDING_PAYMENT"})
    @DisplayName("取消订单 - 合法状态取消成功")
    void testCancelOrder_ValidStatus(String status) {
        testOrder.setStatus(status);
        when(orderRepository.findById(1L)).thenReturn(Optional.of(testOrder));

        orderService.cancelOrder(1L);

        assertEquals("CANCELLED", testOrder.getStatus());
    }
}

2.2 Mockito 高级用法

// 使用 ArgumentCaptor 捕获参数
@Test
void testSaveOrder_CaptureArgument() {
    orderService.createOrder(1001L, BigDecimal.valueOf(99.99));

    ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
    verify(orderRepository).save(captor.capture());

    Order saved = captor.getValue();
    assertThat(saved.getUserId()).isEqualTo(1001L);
    assertThat(saved.getTotalAmount()).isEqualByComparingTo("99.99");
}

// 验证调用顺序
@Test
void testOrderProcess_ExecutionOrder() {
    InOrder inOrder = inOrder(orderRepository, paymentClient);

    orderService.processOrder(1L);

    inOrder.verify(orderRepository).findById(1L);
    inOrder.verify(paymentClient).charge(any());
    inOrder.verify(orderRepository).save(any());
}

// doThrow / doAnswer
@Test
void testPaymentFailure_Compensation() {
    doThrow(new PaymentException("Insufficient funds"))
        .when(paymentClient).charge(any());

    assertThrows(PaymentException.class, () -> {
        orderService.processOrder(1L);
    });

    // 验证补偿操作
    verify(orderRepository).save(argThat(o -> o.getStatus().equals("CANCELLED")));
}

2.3 测试覆盖率

覆盖率类型 含义 目标
行覆盖率(Line) 代码行执行比例 ≥ 80%
分支覆盖率(Branch) 条件分支覆盖比例 ≥ 70%
方法覆盖率(Method) 方法被测试调用比例 ≥ 80%
路径覆盖率(Path) 所有可能路径覆盖 核心逻辑 ≥ 80%


    org.jacoco
    jacoco-maven-plugin
    0.8.11
    
        
            **/dto/**
            **/config/**
        
        
            
                CLASS
                
                    
                        LINE
                        COVEREDRATIO
                        0.80
                    
                
            
        
    
    
        
            prepare-agent
        
        
            report
            verify
            report
        
        
            check
            verify
            check
        
    

三、代码审查与静态分析

3.1 代码审查规范

审查清单

维度 检查项
正确性 业务逻辑是否正确?边界条件是否处理?
安全性 是否存在注入风险?敏感数据是否加密?
性能 是否存在 N+1 查询?是否创建了不必要的对象?
可读性 命名是否清晰?方法是否过长?注释是否必要?
可测试性 依赖是否可以 Mock?是否方便写单元测试?
可维护性 是否遵循单一职责?是否引入了不必要的复杂度?

3.2 SonarQube 静态分析

# sonar-project.properties
sonar.projectKey=order-service
sonar.projectName=Order Service
sonar.projectVersion=1.0
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.java.binaries=target/classes
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
sonar.exclusions=**/dto/**,**/config/**,**/*Application.java
sonar.qualitygate.wait=true

SonarQube 核心规则分类:

// Bug 检测
@PostConstruct  // ❌ @PostConstruct 在构造函数之前调用,其中依赖可能为 null
public void init() {
    this.defaultConfig = loadConfig("default.yaml");
}

// ✅ 正确方式
@PostConstruct
public void init() {
    if (configService != null) {  // 空指针防护
        this.defaultConfig = configService.loadConfig("default.yaml");
    }
}

// 代码异味——太长的 Lambda
public List<Order> findOrders() {
    return orderRepository.findAll().stream()
        .filter(o -> o.getStatus() != null && o.getStatus().equals("ACTIVE") 
            && o.getUserId() != null && o.getUserId() > 0)
        .collect(Collectors.toList());
}
// ✅ 提取方法
public List<Order> findOrders() {
    return orderRepository.findAll().stream()
        .filter(this::isActiveOrder)
        .collect(Collectors.toList());
}
private boolean isActiveOrder(Order o) {
    return o.getStatus() != null && o.getStatus().equals("ACTIVE") 
        && o.getUserId() != null && o.getUserId() > 0;
}

四、Git 工作流

4.1 Git Flow vs Trunk Based

graph TD
    subgraph GitFlow[Git Flow]
        M[master] --> R1[release/1.0]
        R1 --> M2[master v1.0 tag]
        D[develop] --> R1
        F1[feature/user-auth] --> D
        F2[feature/order-api] --> D
        H[hotfix/critical-bug] --> M
        M2 -.->|merge back| D
    end

    subgraph Trunk[Trunk Based]
        T[main] --> F3[feature/user-auth]
        F3 --> T
        T --> F4[feature/order-api]
        F4 --> T
        T1[main v1.0 tag]
    end
特性 Git Flow Trunk Based
分支数量 多(develop, release, feature, hotfix) 少(main + 短期 feature)
集成频率 每 Feature 分支集成一次 每日集成多次
发布周期 版本化发布 持续发布
适合团队 大型团队、版本化产品 小团队、SaaS 产品
冲突解决 集成时集中解决 风险小,及早暴露

4.2 分支命名规范

# Feature 分支
feature/user-auth-system   # 新功能
feature/JIRA-123-user-auth  # 关联 JIRA 工单

# Bugfix 分支
bugfix/order-null-pointer   # 非紧急 Bug
hotfix/critical-payment-fix # 紧急修复(从 master 分叉)

# 发布分支
release/1.2.3               # 准备发布

# 版本标签
git tag -a v1.2.3 -m "Release version 1.2.3"

4.3 提交信息规范

# Conventional Commits 规范
(): 

# 类型
feat:    新功能
fix:     Bug 修复
docs:    文档变更
style:   代码格式(无逻辑变更)
refactor:代码重构
perf:    性能优化
test:    添加测试
chore:   构建/工具变更
ci:      CI 配置变更

# 示例
feat(order): add order cancellation with refund support
fix(user): handle null email in password reset flow
refactor(payment): extract payment validation to separate service
perf(cache): reduce Redis TTL for hot product keys
test(order): add cancellation test for all statuses

五、CI/CD 流水线

5.1 GitHub Actions

name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: test
          MYSQL_DATABASE: testdb
        ports:
          - 3306:3306
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379

    steps:
    - uses: actions/checkout@v4

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

    - name: Run tests with coverage
      run: mvn clean verify -Pcoverage

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

    - name: SonarQube Scan
      uses: sonarsource/sonarqube-scan-action@master
      env:
        SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

  build:
    needs: test
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    - name: Build & Package
      run: mvn package -DskipTests -Pproduction

    - name: Build Docker image
      run: |
        docker build -t order-service:${{ github.sha }} .
        docker tag order-service:${{ github.sha }} \
          ghcr.io/${{ github.repository }}:${{ github.sha }}

    - name: Push to GitHub Container Registry
      run: |
        echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
        docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production

    steps:
    - name: Deploy to Kubernetes
      run: |
        kubectl set image deployment/order-service \
          order-service=ghcr.io/${{ github.repository }}:${{ github.sha }} \
          --namespace production

5.2 Jenkins Pipeline

pipeline {
    agent any

    environment {
        DOCKER_REGISTRY = 'registry.example.com'
        APP_NAME = 'order-service'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Unit Test') {
            steps {
                sh 'mvn test -Pcoverage'
            }
            post {
                success {
                    junit 'target/surefire-reports/*.xml'
                    jacoco classPattern: 'target/classes'
                }
            }
        }

        stage('SonarQube Analysis') {
            steps {
                withSonarQubeEnv('SonarQube') {
                    sh 'mvn sonar:sonar'
                }
            }
        }

        stage('Quality Gate') {
            steps {
                timeout(time: 5, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }

        stage('Build Docker Image') {
            steps {
                sh """
                    docker build -t ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} .
                    docker tag ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} \
                        ${DOCKER_REGISTRY}/${APP_NAME}:latest
                """
            }
        }

        stage('Deploy to Staging') {
            steps {
                sh 'kubectl apply -f k8s/staging/'
                sh 'kubectl rollout status deployment/${APP_NAME} -n staging'
            }
        }

        stage('Integration Test') {
            steps {
                sh 'mvn verify -Pintegration-test'
            }
        }

        stage('Deploy to Production') {
            input {
                message "Deploy to production?"
                ok "Proceed"
            }
            steps {
                sh 'kubectl apply -f k8s/production/'
                sh 'kubectl rollout status deployment/${APP_NAME} -n production'
            }
        }
    }

    post {
        success {
            emailext(
                subject: "Pipeline Success: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                body: "Deployment successful!",
                to: "team@example.com"
            )
        }
        failure {
            emailext(
                subject: "Pipeline Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                body: "Pipeline failed. Please check Jenkins.",
                to: "team@example.com"
            )
        }
    }
}

六、发布策略

6.1 策略对比

策略 描述 风险 复杂度
蓝绿部署 两套完整环境,切换流量
灰度发布 逐步增加新版本流量比例
金丝雀发布 少部分用户先用新版
滚动更新 逐个替换实例
# Kubernetes 滚动更新
apiVersion: apps/v1
kind: Deployment
spec:
  replicas: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2
      maxUnavailable: 1
  template:
    spec:
      containers:
      - name: order-service
        image: order-service:v2.0.0
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5

6.2 优雅关闭

@Configuration
public class GracefulShutdownConfig {

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.addConnectorCustomizers(connector -> {
            connector.setProperty("acceptCount", "100");
        });
        return factory;
    }
}
# Spring Boot 优雅关闭
server:
  shutdown: graceful  # 优雅关闭,等待请求处理完成
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s  # 最多等待 30 秒

七、总结

代码质量与工程化不是一个单一工具或流程能够解决的问题,而是一套完整的体系:

  1. 单元测试:JUnit 5 + Mockito + JaCoCo 构成了测试金字塔的底层基础,覆盖率达到 80% 是保障质量的基本门槛
  2. 代码审查:结合静态分析工具(SonarQube),在代码合并前自动发现缺陷和代码异味
  3. Git 工作流:Trunk Based 适合持续交付场景,Git Flow 适合版本化发布,团队应选择适合自身的模式
  4. CI/CD 流水线:每次代码提交都触发自动构建、测试和部署,将质量门禁自动化
  5. 发布策略:滚动更新、蓝绿部署、金丝雀发布在不同场景下各有优势

工程化的最终目标是:让交付高质量软件成为确定性流程,而非依赖个人英雄主义。当每次代码提交都能自动运行测试、分析质量、构建镜像、部署环境时,团队就可以将精力集中在真正的业务创新上。


代码质量与工程化——从单元测试到 CI/CD 全链路

代码质量与工程化——从单元测试到 CI/CD 全链路

一、引言

代码质量是软件工程的永恒命题。良好的代码质量不仅意味着更少的 Bug,更意味着更低的维护成本、更快的交付速度和更顺畅的团队协作。然而,在追求交付速度的压力下,质量往往成为首当其冲的牺牲品。

工程化的核心思想是将软件开发从”手工作坊”升级为”工业流水线”——通过自动化工具、标准化流程和系统化的质量门禁,在保证效率的同时确保质量。本文将从单元测试出发,依次覆盖代码审查规范、Git 工作流和 CI/CD 流水线,最后介绍发布策略,帮助团队构建完整的质量保障体系。

二、单元测试

2.1 JUnit 5 核心特性

// JUnit 5 基础测试
@SpringBootTest
@AutoConfigureMockMvc
class OrderServiceTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private OrderRepository orderRepository;

    @InjectMocks
    @Autowired
    private OrderService orderService;

    private Order testOrder;

    @BeforeEach
    void setUp() {
        testOrder = new Order();
        testOrder.setId(1L);
        testOrder.setOrderNo("ORD20260516001");
        testOrder.setUserId(1001L);
        testOrder.setStatus("CREATED");
    }

    @Test
    @DisplayName("创建订单 - 成功场景")
    void testCreateOrder_Success() {
        when(orderRepository.save(any(Order.class)))
            .thenReturn(testOrder);

        Order result = orderService.createOrder(1001L, BigDecimal.valueOf(99.99));

        assertNotNull(result);
        assertEquals("ORD20260516001", result.getOrderNo());
        assertEquals("CREATED", result.getStatus());
        verify(orderRepository).save(any(Order.class));
    }

    @Test
    @DisplayName("取消订单 - 仅允许待支付状态")
    void testCancelOrder_OnlyPendingAllowed() {
        Order paidOrder = new Order();
        paidOrder.setId(2L);
        paidOrder.setStatus("PAID");
        when(orderRepository.findById(2L)).thenReturn(Optional.of(paidOrder));

        assertThrows(IllegalStateException.class, () -> {
            orderService.cancelOrder(2L);
        });
    }

    @ParameterizedTest
    @ValueSource(strings = {"CREATED", "PENDING_PAYMENT"})
    @DisplayName("取消订单 - 合法状态取消成功")
    void testCancelOrder_ValidStatus(String status) {
        testOrder.setStatus(status);
        when(orderRepository.findById(1L)).thenReturn(Optional.of(testOrder));

        orderService.cancelOrder(1L);

        assertEquals("CANCELLED", testOrder.getStatus());
    }
}

2.2 Mockito 高级用法

// 使用 ArgumentCaptor 捕获参数
@Test
void testSaveOrder_CaptureArgument() {
    orderService.createOrder(1001L, BigDecimal.valueOf(99.99));

    ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
    verify(orderRepository).save(captor.capture());

    Order saved = captor.getValue();
    assertThat(saved.getUserId()).isEqualTo(1001L);
    assertThat(saved.getTotalAmount()).isEqualByComparingTo("99.99");
}

// 验证调用顺序
@Test
void testOrderProcess_ExecutionOrder() {
    InOrder inOrder = inOrder(orderRepository, paymentClient);

    orderService.processOrder(1L);

    inOrder.verify(orderRepository).findById(1L);
    inOrder.verify(paymentClient).charge(any());
    inOrder.verify(orderRepository).save(any());
}

// doThrow / doAnswer
@Test
void testPaymentFailure_Compensation() {
    doThrow(new PaymentException("Insufficient funds"))
        .when(paymentClient).charge(any());

    assertThrows(PaymentException.class, () -> {
        orderService.processOrder(1L);
    });

    // 验证补偿操作
    verify(orderRepository).save(argThat(o -> o.getStatus().equals("CANCELLED")));
}

2.3 测试覆盖率

覆盖率类型 含义 目标
行覆盖率(Line) 代码行执行比例 ≥ 80%
分支覆盖率(Branch) 条件分支覆盖比例 ≥ 70%
方法覆盖率(Method) 方法被测试调用比例 ≥ 80%
路径覆盖率(Path) 所有可能路径覆盖 核心逻辑 ≥ 80%


    org.jacoco
    jacoco-maven-plugin
    0.8.11
    
        
            **/dto/**
            **/config/**
        
        
            
                CLASS
                
                    
                        LINE
                        COVEREDRATIO
                        0.80
                    
                
            
        
    
    
        
            prepare-agent
        
        
            report
            verify
            report
        
        
            check
            verify
            check
        
    

三、代码审查与静态分析

3.1 代码审查规范

审查清单

维度 检查项
正确性 业务逻辑是否正确?边界条件是否处理?
安全性 是否存在注入风险?敏感数据是否加密?
性能 是否存在 N+1 查询?是否创建了不必要的对象?
可读性 命名是否清晰?方法是否过长?注释是否必要?
可测试性 依赖是否可以 Mock?是否方便写单元测试?
可维护性 是否遵循单一职责?是否引入了不必要的复杂度?

3.2 SonarQube 静态分析

# sonar-project.properties
sonar.projectKey=order-service
sonar.projectName=Order Service
sonar.projectVersion=1.0
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.java.binaries=target/classes
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
sonar.exclusions=**/dto/**,**/config/**,**/*Application.java
sonar.qualitygate.wait=true

SonarQube 核心规则分类:

// Bug 检测
@PostConstruct  // ❌ @PostConstruct 在构造函数之前调用,其中依赖可能为 null
public void init() {
    this.defaultConfig = loadConfig("default.yaml");
}

// ✅ 正确方式
@PostConstruct
public void init() {
    if (configService != null) {  // 空指针防护
        this.defaultConfig = configService.loadConfig("default.yaml");
    }
}

// 代码异味——太长的 Lambda
public List<Order> findOrders() {
    return orderRepository.findAll().stream()
        .filter(o -> o.getStatus() != null && o.getStatus().equals("ACTIVE") 
            && o.getUserId() != null && o.getUserId() > 0)
        .collect(Collectors.toList());
}
// ✅ 提取方法
public List<Order> findOrders() {
    return orderRepository.findAll().stream()
        .filter(this::isActiveOrder)
        .collect(Collectors.toList());
}
private boolean isActiveOrder(Order o) {
    return o.getStatus() != null && o.getStatus().equals("ACTIVE") 
        && o.getUserId() != null && o.getUserId() > 0;
}

四、Git 工作流

4.1 Git Flow vs Trunk Based

graph TD
    subgraph GitFlow[Git Flow]
        M[master] --> R1[release/1.0]
        R1 --> M2[master v1.0 tag]
        D[develop] --> R1
        F1[feature/user-auth] --> D
        F2[feature/order-api] --> D
        H[hotfix/critical-bug] --> M
        M2 -.->|merge back| D
    end

    subgraph Trunk[Trunk Based]
        T[main] --> F3[feature/user-auth]
        F3 --> T
        T --> F4[feature/order-api]
        F4 --> T
        T1[main v1.0 tag]
    end
特性 Git Flow Trunk Based
分支数量 多(develop, release, feature, hotfix) 少(main + 短期 feature)
集成频率 每 Feature 分支集成一次 每日集成多次
发布周期 版本化发布 持续发布
适合团队 大型团队、版本化产品 小团队、SaaS 产品
冲突解决 集成时集中解决 风险小,及早暴露

4.2 分支命名规范

# Feature 分支
feature/user-auth-system   # 新功能
feature/JIRA-123-user-auth  # 关联 JIRA 工单

# Bugfix 分支
bugfix/order-null-pointer   # 非紧急 Bug
hotfix/critical-payment-fix # 紧急修复(从 master 分叉)

# 发布分支
release/1.2.3               # 准备发布

# 版本标签
git tag -a v1.2.3 -m "Release version 1.2.3"

4.3 提交信息规范

# Conventional Commits 规范
(): 

# 类型
feat:    新功能
fix:     Bug 修复
docs:    文档变更
style:   代码格式(无逻辑变更)
refactor:代码重构
perf:    性能优化
test:    添加测试
chore:   构建/工具变更
ci:      CI 配置变更

# 示例
feat(order): add order cancellation with refund support
fix(user): handle null email in password reset flow
refactor(payment): extract payment validation to separate service
perf(cache): reduce Redis TTL for hot product keys
test(order): add cancellation test for all statuses

五、CI/CD 流水线

5.1 GitHub Actions

name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: test
          MYSQL_DATABASE: testdb
        ports:
          - 3306:3306
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379

    steps:
    - uses: actions/checkout@v4

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

    - name: Run tests with coverage
      run: mvn clean verify -Pcoverage

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

    - name: SonarQube Scan
      uses: sonarsource/sonarqube-scan-action@master
      env:
        SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

  build:
    needs: test
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    - name: Build & Package
      run: mvn package -DskipTests -Pproduction

    - name: Build Docker image
      run: |
        docker build -t order-service:${{ github.sha }} .
        docker tag order-service:${{ github.sha }} \
          ghcr.io/${{ github.repository }}:${{ github.sha }}

    - name: Push to GitHub Container Registry
      run: |
        echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
        docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production

    steps:
    - name: Deploy to Kubernetes
      run: |
        kubectl set image deployment/order-service \
          order-service=ghcr.io/${{ github.repository }}:${{ github.sha }} \
          --namespace production

5.2 Jenkins Pipeline

pipeline {
    agent any

    environment {
        DOCKER_REGISTRY = 'registry.example.com'
        APP_NAME = 'order-service'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Unit Test') {
            steps {
                sh 'mvn test -Pcoverage'
            }
            post {
                success {
                    junit 'target/surefire-reports/*.xml'
                    jacoco classPattern: 'target/classes'
                }
            }
        }

        stage('SonarQube Analysis') {
            steps {
                withSonarQubeEnv('SonarQube') {
                    sh 'mvn sonar:sonar'
                }
            }
        }

        stage('Quality Gate') {
            steps {
                timeout(time: 5, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }

        stage('Build Docker Image') {
            steps {
                sh """
                    docker build -t ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} .
                    docker tag ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} \
                        ${DOCKER_REGISTRY}/${APP_NAME}:latest
                """
            }
        }

        stage('Deploy to Staging') {
            steps {
                sh 'kubectl apply -f k8s/staging/'
                sh 'kubectl rollout status deployment/${APP_NAME} -n staging'
            }
        }

        stage('Integration Test') {
            steps {
                sh 'mvn verify -Pintegration-test'
            }
        }

        stage('Deploy to Production') {
            input {
                message "Deploy to production?"
                ok "Proceed"
            }
            steps {
                sh 'kubectl apply -f k8s/production/'
                sh 'kubectl rollout status deployment/${APP_NAME} -n production'
            }
        }
    }

    post {
        success {
            emailext(
                subject: "Pipeline Success: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                body: "Deployment successful!",
                to: "team@example.com"
            )
        }
        failure {
            emailext(
                subject: "Pipeline Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                body: "Pipeline failed. Please check Jenkins.",
                to: "team@example.com"
            )
        }
    }
}

六、发布策略

6.1 策略对比

策略 描述 风险 复杂度
蓝绿部署 两套完整环境,切换流量
灰度发布 逐步增加新版本流量比例
金丝雀发布 少部分用户先用新版
滚动更新 逐个替换实例
# Kubernetes 滚动更新
apiVersion: apps/v1
kind: Deployment
spec:
  replicas: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2
      maxUnavailable: 1
  template:
    spec:
      containers:
      - name: order-service
        image: order-service:v2.0.0
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5

6.2 优雅关闭

@Configuration
public class GracefulShutdownConfig {

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.addConnectorCustomizers(connector -> {
            connector.setProperty("acceptCount", "100");
        });
        return factory;
    }
}
# Spring Boot 优雅关闭
server:
  shutdown: graceful  # 优雅关闭,等待请求处理完成
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s  # 最多等待 30 秒

七、总结

代码质量与工程化不是一个单一工具或流程能够解决的问题,而是一套完整的体系:

  1. 单元测试:JUnit 5 + Mockito + JaCoCo 构成了测试金字塔的底层基础,覆盖率达到 80% 是保障质量的基本门槛
  2. 代码审查:结合静态分析工具(SonarQube),在代码合并前自动发现缺陷和代码异味
  3. Git 工作流:Trunk Based 适合持续交付场景,Git Flow 适合版本化发布,团队应选择适合自身的模式
  4. CI/CD 流水线:每次代码提交都触发自动构建、测试和部署,将质量门禁自动化
  5. 发布策略:滚动更新、蓝绿部署、金丝雀发布在不同场景下各有优势

工程化的最终目标是:让交付高质量软件成为确定性流程,而非依赖个人英雄主义。当每次代码提交都能自动运行测试、分析质量、构建镜像、部署环境时,团队就可以将精力集中在真正的业务创新上。


CI 镜像安全扫描

CI 镜像安全扫描

为什么需要镜像安全扫描

Docker 镜像本质上是一个 Linux 文件系统 + 应用文件的集合。基础镜像中可能包含大量已知漏洞(CVEs)的软件包,不扫描就直接上线会带来严重的安全风险。

常见漏洞扫描工具

1. Trivy(推荐)

# 安装
sudo apt-get install trivy

# 扫描本地镜像
trivy image nginx:latest

# 扫描 Dockerfile
trivy fs .

# 扫描容器
trivy container myapp

输出示例:

nginx:latest (debian 11.6)
==========================
Total: 45 (UNKNOWN: 0, LOW: 15, MEDIUM: 25, HIGH: 4, CRITICAL: 1)

┌─────────┬────────────────┬──────────┬──────────────────────────┐
│ LIBRARY │ VULNERABILITY  │ SEVERITY │ INSTALLED VERSION        │
├─────────┼────────────────┼──────────┼──────────────────────────┤
│ openssl │ CVE-2023-xxxxx │ CRITICAL │ 1.1.1n-0+deb11u2         │
│ libc6   │ CVE-2023-yyyyy │ HIGH     │ 2.31-13+deb11u3          │
└─────────┴────────────────┴──────────┴──────────────────────────┘

2. Docker Scout

# Docker Desktop 内置
docker scout quickview nginx:latest

# 详细报告
docker scout recommendations nginx:latest

# 比较镜像
docker scout compare nginx:1.25 nginx:1.24

3. Clair

CoreOS 开源的静态分析工具,常与 Harbor 镜像仓库集成。

4. Anchore Grype

grype nginx:latest

CI 集成示例

GitHub Actions + Trivy

name: Build and Scan

on:
  push:
    branches: [main]

jobs:
  build-and-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          format: 'table'
          exit-code: '1'       # 发现漏洞时让 CI 失败
          severity: 'CRITICAL,HIGH'  # 只关注高危及以上

GitLab CI + Trivy

scan:
  stage: test
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  script:
    - trivy image --severity CRITICAL,HIGH --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

漏洞分级与策略

严重等级 处理策略
CRITICAL 阻塞发布,必须在镜像构建阶段修复
HIGH 建议修复,阻塞策略可选
MEDIUM 记录跟踪,定期修复
LOW 忽略或记录

构建阶段修复漏洞

# 在构建阶段就修复已知漏洞
FROM ubuntu:22.04 AS base
RUN apt-get update && apt-get upgrade -y

FROM node:18 AS node-base
RUN apt-get update && apt-get upgrade -y && \
    npm audit fix

最佳实践

  1. 基础镜像最小化:使用 alpinedistroless 减少攻击面
  2. 定期扫描:不只是 CI 构建时扫描,基础镜像更新后也要重新扫描
  3. CVE 白名单:部分 CVE(如内核相关)不影响容器运行时,可设白名单
  4. 多阶段构建:最终运行镜像只包含运行所需的最小文件
  5. 签名验证:使用 cosign 对镜像签名,防止被篡改

面试要点

  1. Trivy 是目前最流行的开源容器安全扫描工具
  2. 扫描应放在 CI 的构建阶段,阻止高危镜像进入仓库
  3. 不仅要扫应用层,基础镜像层也要定期扫描
  4. 最小化基础镜像 + 及时更新 = 最佳安全组合
  5. 镜像签名和准入控制是企业级安全的关键

面试官常问:如果扫描发现一个高危漏洞但不影响你的业务,怎么处理?


金丝雀发布

金丝雀发布

什么是金丝雀发布

金丝雀发布(Canary Release)是一种渐进式的发布策略,先让一小部分用户使用新版本,验证稳定后再逐步扩大范围,最终全量上线。

名称源自”煤矿中的金丝雀”——矿工曾用金丝雀检测有毒气体,如果金丝雀死亡就说明环境危险,需要撤离。

金丝雀发布流程

v1: [全部 100% 流量]
         ↓
v1: [95%] ─── v2: [5%]  ← 观察指标
         ↓ (没问题,继续扩大)
v1: [80%] ─── v2: [20%]
         ↓
v1: [50%] ─── v2: [50%]
         ↓
v1: [0%] ─── v2: [100%]  ← 全量完成

Docker 环境下的金丝雀实现

方式一:基于 Docker Compose + 负载均衡

# docker-compose.canary.yml
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx-canary.conf:/etc/nginx/nginx.conf

  app-v1:
    image: myapp:1.0.0
    container_name: app-v1

  app-v2:
    image: myapp:2.0.0
    container_name: app-v2

Nginx 配置实现流量分割:

upstream backend {
    server app-v1:8080 weight=90;   # 90% 流量到 v1
    server app-v2:8080 weight=10;   # 10% 流量到 v2
}

server {
    listen 80;
    location / {
        proxy_pass http://backend;
    }
}

方式二:基于网关

# 基于 Header 或 Cookie 进行更加精细的控制
upstream app-v1 {
    server app-v1:8080;
}

upstream app-v2 {
    server app-v2:8080;
}

server {
    listen 80;

    location / {
        # 如果请求头包含 canary=yes,走 v2 版本
        if ($http_x_canary = "yes") {
            proxy_pass http://app-v2;
            break;
        }
        proxy_pass http://app-v1;
    }
}

方式三:Docker Swarm 滚动更新

# 创建服务
docker service create --name myapp --replicas 10 myapp:1.0.0

# 金丝雀更新:先更新 1 个实例
docker service update \
  --image myapp:2.0.0 \
  --update-parallelism 1 \
  --update-delay 30s \
  myapp

金丝雀发布的指标监控

部署新版本后必须监控的关键指标:

# Prometheus 告警配置
groups:
  - name: canary
    rules:
      - alert: CanaryErrorRate
        expr: |
          rate(http_requests_total{version="v2", status=~"5.."}[5m])
          /
          rate(http_requests_total{version="v2"}[5m]) > 0.01
        for: 2m
        annotations:
          summary: "金丝雀版本错误率超过 1%"

金丝雀 vs 蓝绿 vs 滚动

特性 金丝雀 蓝绿 滚动
流量控制 精确百分比 全量切换 按实例数
发布速度 慢(逐步) 快(瞬时) 中等
风险评估 最低 中等
复杂程度
A/B 测试 天然支持 不支持 不支持

面试要点

  1. 金丝雀发布是最安全的发布策略之一,适合重大版本变更
  2. Docker 环境结合 Nginx/Traefik 可以实现灵活的流量控制
  3. 关键在于监控指标的准确性——如果指标不准,金丝雀就失去了意义
  4. 自动回滚策略:监控到错误率上升后自动切换回旧版本
  5. 数据兼容性:新旧版本必须能同时访问同一数据源

面试官常问:金丝雀发布你们用了哪些灰度策略?如果发现新版本有问题怎么处理?


蓝绿部署与滚动部署

蓝绿部署与滚动部署

蓝绿部署(Blue-Green Deployment)

概念

蓝绿部署维护两套完全相同的生产环境:蓝环境(当前在线)和绿环境(新版本)。

用户 → 负载均衡器 → 蓝环境(v1,当前运行)
                    绿环境(v2,初始空闲)

切换流程

# 1. 部署新版本到绿环境
docker-compose -f docker-compose.green.yml up -d

# 2. 验证绿环境健康
curl http://green.example.com/health

# 3. 切换负载均衡到绿环境(从蓝切到绿)
# Nginx 方式:
# 修改 upstream 配置指向绿环境
# nginx -s reload

# Docker 方式(简单演示):
# 停止蓝环境的流量入口
docker stop nginx-blue

# 启动指向绿环境的入口
docker run -d --name nginx-green \
  -v ./nginx-green.conf:/etc/nginx/nginx.conf \
  -p 80:80 \
  nginx:alpine

# 4. 旧版本保留一段时间
# 如出现问题,可快速回滚(切回蓝环境)

回滚

# 只需重新切回蓝环境
docker stop nginx-green
docker start nginx-blue

Docker Compose 蓝绿实现

# docker-compose.blue.yml
services:
  app:
    image: myapp:1.0.0
    container_name: app-blue
    networks:
      - app-net
    ports:
      - "8081:8080"
# docker-compose.green.yml
services:
  app:
    image: myapp:2.0.0
    container_name: app-green
    networks:
      - app-net
    ports:
      - "8082:8080"

滚动部署(Rolling Deployment)

概念

滚动部署逐步替换旧实例为新版本,不维护两个完整的环境。

初始: [v1] [v1] [v1] [v1] [v1]
步骤1: [v2] [v1] [v1] [v1] [v1]  # 停止一个 v1启动一个 v2
步骤2: [v2] [v2] [v1] [v1] [v1]
步骤3: [v2] [v2] [v2] [v1] [v1]
步骤4: [v2] [v2] [v2] [v2] [v1]
完成: [v2] [v2] [v2] [v2] [v2]

Docker Compose 滚动部署

# 使用 --scale 实现滚动更新
docker-compose up -d --scale app=5 --no-recreate

# 逐个更新容器
for i in $(seq 1 5); do
  docker service update --image myapp:2.0.0 app_$i
  sleep 10  # 等待新版本就绪
done

健康检查

# docker-compose.yml
services:
  app:
    image: myapp:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 15s

两种方案对比

特性 蓝绿部署 滚动部署
资源需求 2倍(完整两份) N+1(额外一个)
切换速度 极快(瞬间切换) 较慢(逐个替换)
回滚速度 极快(切回即可) 较慢(逐个回滚)
流量影响 无(瞬时切换) 平滑过渡
成本
适用场景 关键服务、数据库变更 无状态服务

面试要点

  1. 蓝绿部署需要两倍资源,但切换和回滚都是瞬间完成的
  2. 滚动部署资源消耗小,适合无状态微服务
  3. 蓝绿部署要做数据兼容性处理(新旧代码可能同时访问同一数据库)
  4. Docker Swarm 原生支持滚动更新
  5. K8s 中蓝绿部署通常通过 Service label selector 切换实现

面试官常问:你们用哪种部署策略?数据库兼容性问题怎么处理?


GitLab CI Docker 集成

GitLab CI Docker 集成

GitLab CI + Docker 的优势

GitLab CI 内置了 Docker 支持,是使用最广泛的 CI/CD 平台之一。它的 Docker 集成深度极高,从代码提交到镜像部署全部在一个平台完成。

GitLab Runner 安装与配置

# 安装 Docker Runner
docker run -d --name gitlab-runner --restart always \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  gitlab/gitlab-runner:latest

# 注册 Runner
docker exec -it gitlab-runner gitlab-runner register \
  --url https://gitlab.com/ \
  --registration-token YOUR_TOKEN \
  --executor docker \
  --docker-image alpine:latest \
  --description "docker-runner"

.gitlab-ci.yml 配置

基础 Docker 构建

stages:
  - build
  - test
  - deploy

variables:
  DOCKER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH

build:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  script:
    - docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA .
    - docker tag $DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA $DOCKER_IMAGE:latest
    - docker push $DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA
    - docker push $DOCKER_IMAGE:latest

多阶段流水线

stages:
  - lint
  - test
  - build
  - deploy

lint:
  stage: lint
  image: node:18
  script:
    - npm ci
    - npm run lint

test:
  stage: test
  image: node:18
  services:
    - postgres:15
  script:
    - npm ci
    - npm test

build:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
  script:
    - ssh deploy@server "docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA && docker-compose up -d"
  only:
    - main

Docker-in-Docker (DinD) 服务

关键配置是 services: - docker:24.0.5-dind,它启动一个 DinD 容器作为服务:

variables:
  DOCKER_TLS_CERTDIR: ""
  DOCKER_HOST: tcp://docker:2375

GitLab Container Registry

GitLab 内置了容器镜像仓库:

variables:
  CI_REGISTRY: registry.gitlab.com
  CI_REGISTRY_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
  CI_REGISTRY_USER: gitlab-ci-token
  CI_REGISTRY_PASSWORD: $CI_JOB_TOKEN

login:
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

高级技巧

使用 GitLab Cache

variables:
  npm_config_cache: /cache

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - node_modules/
    - .npm/

多架构构建

variables:
  DOCKER_BUILDKIT: 1
  DOCKER_CLI_EXPERIMENTAL: enabled

build:multiarch:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  script:
    - docker buildx create --use
    - docker buildx build --platform linux/amd64,linux/arm64 -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA --push .

面试要点

  1. GitLab CI 通过 DinD 服务实现容器内的 Docker 构建
  2. GitLab Container Registry 提供私有镜像仓库能力
  3. 使用 CI_JOB_TOKEN 实现安全的镜像推送
  4. 多架构构建需要 Buildx + QEMU
  5. GitLab Runner 可以注册 Docker、Kubernets 等多种 executor

面试官常问:GitLab CI 中 DinD 如何工作?有什么安全风险?


GitHub Actions 推送镜像

GitHub Actions 推送镜像

GitHub Actions + Docker 集成

GitHub Actions 是 GitHub 原生的 CI/CD 平台,集成了 Docker 构建和推送的最佳支持。

基础配置:构建并推送到 Docker Hub

name: Build and Push Docker Image

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: |
            yourusername/myapp:latest
            yourusername/myapp:${{ github.sha }}

推送到 GitHub Container Registry (GHCR)

name: Push to GHCR

on:
  push:
    branches: [main]

jobs:
  push-to-ghcr:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}

多架构镜像构建

jobs:
  buildx:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push multi-platform
        uses: docker/build-push-action@v5
        with:
          push: true
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          tags: yourusername/myapp:latest

构建缓存优化

- name: Build with cache
  uses: docker/build-push-action@v5
  with:
    push: true
    cache-from: type=gha
    cache-to: type=gha,mode=max
    tags: yourusername/myapp:latest

使用 GitHub Actions 的 gha 缓存类型,构建层缓存存储在 GitHub 的缓存服务中。

镜像安全扫描

- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'yourusername/myapp:${{ github.sha }}'
    format: 'sarif'
    output: 'trivy-results.sarif'

- name: Upload Trivy results to GitHub Security
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: 'trivy-results.sarif'

Secrets 管理

GitHub Actions 通过 Secrets 管理敏感信息:

Secret 名称 用途
DOCKER_USERNAME Docker Hub 用户名
DOCKER_PASSWORD Docker Hub 密码
GITHUB_TOKEN 自动可用,无需设置

面试要点

  1. GitHub Actions 提供标准的 Docker Action 简化构建流程
  2. docker/build-push-action 支持多平台、缓存、推送一站式
  3. GHCR 是 Docker Hub 的优秀替代方案(免费、无拉取限制)
  4. 缓存使用 type=gha 是 GitHub Actions 的最佳实践
  5. Trivy 集成可以在推送前发现镜像漏洞

面试官常问:GitHub Actions 和 GitLab CI 的 Docker 集成有什么异同?


CI/CD 中 Docker 的角色

CI/CD 中 Docker 的角色

Docker 为什么在 CI/CD 中如此重要

传统 CI/CD 面临的最大问题是”环境不一致”——开发环境能跑,测试环境挂了,生产环境又出问题。Docker 容器提供了可移植、可重现的环境,完美解决了这个问题。

Docker 在 CI/CD 中的核心价值

1. 环境一致性

# 从开发到生产使用同一镜像
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 nodejs
COPY app/ /app
CMD ["python3", "/app/app.py"]

开发、测试、生产使用相同的 Dockerfile,消除”在我机器上能跑”的问题。

2. 构建隔离

每个构建任务运行在独立的容器中,互不干扰:
– 不污染宿主机的依赖
– 构建结束后自动清理
– 支持并行构建

3. 可重现的构建

docker build -t myapp:${GIT_COMMIT} .

Git commit 和 Docker 镜像一一对应,可以随时回退到任意版本。

4. 快速部署

代码提交 → 构建镜像 → 推送镜像仓库 → 部署到服务器
           (一次打包,到处运行)

CI/CD 流水线中的 Docker 流程

┌─────────┐   ┌──────────┐   ┌─────────┐   ┌─────────┐
│ 代码提交 │ → │ 单元测试  │ → │ 构建镜像 │ → │ 推送镜像 │
└─────────┘   └──────────┘   └─────────┘   └────┬────┘
                                                 │
┌─────────┐   ┌──────────┐   ┌─────────┐        │
│ 部署生产  │ ← │ 验收测试   │ ← │ 部署测试  │ ← ────┘
└─────────┘   └──────────┘   └─────────┘

不同阶段的 Docker 用法

构建阶段

docker build -t myapp:latest .
docker tag myapp:latest registry.example.com/myapp:1.0.0

测试阶段

docker run --rm myapp:latest npm test
docker-compose -f docker-compose.test.yml up --abort-on-container-exit

部署阶段

# 蓝绿部署
docker pull registry.example.com/myapp:1.0.0
docker stop app-blue && docker rm app-blue
docker run -d --name app-green registry.example.com/myapp:1.0.0

面试要点

  1. Docker 解决了 CI/CD 中最头疼的环境一致性问题
  2. 构建、测试、部署三个环节都受益于容器化
  3. 每提交一次代码都生成一个不可变镜像,版本可追溯
  4. Docker Compose 在集成测试中极其实用
  5. 配合镜像仓库(Docker Hub / Harbor)实现完整的交付链路

面试官常问:你们的 CI/CD 流程是怎么样的?Docker 在其中的具体角色是什么?


镜像安全扫描

镜像安全扫描

为什么需要镜像安全扫描

Docker 镜像基于分层构建,每一层都可能引入漏洞。基础镜像中的已知 CVE、第三方库漏洞、错误配置等都会传递到最终容器中。镜像安全扫描可以自动化检测这些风险。

常见漏洞类型

# 1. 操作系统包漏洞
#   - OpenSSL 心脏出血 (CVE-2014-0160)
#   - glibc Ghost (CVE-2015-0235)

# 2. 语言依赖漏洞
#   - npm 包 (left-pad, event-stream)
#   - pip 包 (urllib3, requests)

# 3. 错误配置
#   - 容器运行在 root 下
#   - 暴露了不必要的端口
#   - 使用了弱加密

# 4. 恶意软件
#   - 挖矿程序
#   - 后门程序
#   - 数据窃取

主流镜像扫描工具

Trivy(推荐)

Trivy 是 Aqua Security 开源的全面漏洞扫描器:

# 安装 Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh

# 扫描镜像
trivy image nginx:latest

# 扫描 Dockerfile
trivy config ./Dockerfile

# 扫描文件系统
trivy fs /path/to/project

# 扫描仓库
trivy repo https://github.com/example/project

Trivy 输出示例:

nginx:latest (debian 11.6)
=========================
Total: 87 (UNKNOWN: 0, LOW: 56, MEDIUM: 24, HIGH: 6, CRITICAL: 1)

┌──────────────┬────────────────┬──────────┬──────────────┐
   Library     Vulnerability   Severity     Status    
├──────────────┼────────────────┼──────────┼──────────────┤
 libssl1.1     CVE-2023-3446   HIGH      fixed in 1.1  
 openssl       CVE-2023-3817   CRITICAL  affected     
└──────────────┴────────────────┴──────────┴──────────────┘

Docker Scout

Docker 官方提供的镜像分析工具:

# 启用 Docker Scout
docker scout quickview nginx:latest

# 扫描并比较
docker scout compare nginx:latest --to nginx:1.24-alpine

# 推荐基础镜像
docker scout recommendations nginx:latest

Clair

CoreOS 开源的静态分析工具:

# 启动 Clair
docker compose -f docker-compose.yml up -d

# 推送镜像到 Clair
clairctl analyze myimage:latest

Snyk

商业级漏洞扫描平台:

# 安装 Snyk CLI
npm install -g snyk

# 认证
snyk auth

# 扫描 Docker 镜像
snyk container test nginx:latest

CI/CD 中集成扫描

GitHub Actions

name: Docker Security Scan

on:
  push:
    branches: [main]

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Build Docker Image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Run Trivy Scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          format: 'sarif'
          output: 'trivy-results.sarif'

      - name: Upload Scan Results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

GitLab CI

container_scanning:
  stage: test
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  variables:
    IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  script:
    - trivy image --exit-code 0 --severity HIGH,CRITICAL $IMAGE_NAME
  only:
    - main

选择基础镜像

镜像漏洞对比

# 扫描常见基础镜像的漏洞数量
trivy image ubuntu:22.04 | grep Total
trivy image debian:11-slim | grep Total
trivy image alpine:3.18 | grep Total
trivy image distroless:base | grep Total
镜像 包管理器 典型漏洞数 镜像大小
ubuntu:22.04 apt ~100 ~77MB
debian:11-slim apt ~50 ~80MB
alpine:3.18 apk ~5 ~5.5MB
distroless/base ~0 ~20MB
scratch 0 0MB

最小化镜像的策略

# ❌ 有大量漏洞的基础镜像
FROM node:18

# ✅ 使用 slim 版本
FROM node:18-slim

# ✅ 使用 Alpine
FROM node:18-alpine

# ✅ 使用 Distroless
FROM gcr.io/distroless/nodejs18-debian11

# ✅ 多阶段构建(最佳)
FROM node:18-alpine AS builder
COPY . .
RUN npm ci && npm run build

FROM node:18-alpine
COPY --from=builder /app/dist /app
CMD ["node", "app.js"]

漏洞修复策略

1. 升级基础镜像

# 定期更新基础镜像
docker pull node:18-alpine

# 在 CI 中固定版本
FROM node:18.16.0-alpine3.18

2. 修复包漏洞

FROM alpine:3.18

# 升级有漏洞的包
RUN apk update && apk upgrade

# 删除不必要的包
RUN apk del curl wget

3. 使用 Distroless 或 scratch

# Scratch 镜像——零漏洞可能
FROM scratch
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]

镜像签名和验证

# Docker Content Trust
export DOCKER_CONTENT_TRUST=1

# 推送时会自动签名
docker push myrepo/myapp:latest

# 拉取时验证签名
docker pull myrepo/myapp:latest
# 如果未签名会失败

扫描策略的最佳实践

  1. 在 CI 中自动扫描:每次构建都扫描
  2. 设置阈值:阻止含有 CRITICAL 级别漏洞的镜像部署
  3. 定期重新扫描:新漏洞不断发现
  4. 最小化基础镜像:减少攻击面
  5. 跟踪修复:记录漏洞的修复状态
  6. 扫描运行时容器:不只是镜像,运行中的容器也需要扫描
# 运行时容器扫描
trivy container running_container_name

# 定期全量扫描(cron)
0 2 * * * trivy image --severity HIGH,CRITICAL $(docker images -q)

镜像安全扫描是容器安全的第一道防线。在 CI/CD 流程中集成扫描,可以防止含有已知漏洞的镜像进入生产环境。


Docker 在 CI/CD 中的关键作用

Docker 在 CI/CD 中的关键作用

Docker 给 CI/CD 带来了什么?

Docker 通过”环境一致性”彻底改变了 CI/CD 流程。简单说就是:开发能用、测试能过、生产能跑的是同一个环境

无 Docker 的传统 CI/CD 痛点

graph TB
    subgraph 传统 CI/CD 痛点
        Dev[开发环境<br/>Python 3.10<br/>GCC 11<br/>Fedora 37]
        CI[CI 服务器<br/>Python 3.8<br/>GCC 9<br/>Ubuntu 20.04]
        Prod[生产环境<br/>Python 3.9<br/>GCC 10<br/>CentOS 7]
    end

    Dev -->|😱 这里能跑| CI
    CI -->|😰 这里报错| Prod

    style Dev fill:#fdd
    style CI fill:#fdd
    style Prod fill:#fdd

有 Docker 的 CI/CD 流程

graph LR
    subgraph 统一环境
        IMG[标准构建镜像<br/>Python 3.10 + GCC 11<br/>容器化]
    end

    subgraph 流水线
        Dev -->|git push| REPO[Git 仓库]
        REPO -->|触发| CIJenkins[CI/CD 服务器]
        CIJenkins -->|docker build| Build
        CIJenkins -->|docker run| Test
        CIJenkins -->|docker push| REG[镜像仓库]
    end

    IMG -.->|同一镜像| Build
    IMG -.->|同一镜像| Test
    IMG -.->|同一镜像| Prod

    REG -->|docker pull| Prod[生产环境]

    subgraph 生产环境
        Prod --> K8S[Kubernetes<br/>docker run]
    end

    style Dev fill:#dfd
    style Test fill:#dfd
    style Prod fill:#dfd

典型 Docker CI/CD 流水线

1. 代码推送触发构建

# .gitlab-ci.yml 示例
stages:
  - build
  - test
  - package
  - deploy

build:
  stage: build
  image: node:18
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/

test:
  stage: test
  image: node:18
  script:
    - npm ci
    - npm test
    - npm run lint

package:
  stage: package
  image: docker:latest
  services:
    - docker:dind  # Docker-in-Docker 支持
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

deploy:
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    - kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

2. Docker 镜像作为不可变交付物

# 构建一次,到处运行
# 构建阶段(CI)
docker build -t myapp:${BUILD_NUMBER} .
docker tag myapp:${BUILD_NUMBER} registry.example.com/myapp:${BUILD_NUMBER}
docker push registry.example.com/myapp:${BUILD_NUMBER}

# 测试阶段(用同一个镜像)
docker run registry.example.com/myapp:${BUILD_NUMBER} npm test

# 部署阶段(用同一个镜像)
docker pull registry.example.com/myapp:${BUILD_NUMBER}
docker run -d -p 80:80 registry.example.com/myapp:${BUILD_NUMBER}

Docker 在 CI/CD 中的关键作用

1. 构建环境标准化

# ci-build.Dockerfile — 统一构建环境
FROM node:18-alpine AS builder
RUN apk add --no-cache python3 make g++
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

2. 并行测试

# 利用容器隔离,并行运行不同版本的测试
docker run --rm myapp:latest "npm run test:unit" &
docker run --rm myapp:latest "npm run test:integration" &
docker run --rm myapp:latest "npm run test:e2e" &
wait

3. 环境变量和配置注入

# 在 CI 中启动依赖服务(数据库、缓存等)
docker run -d --name test-db -e POSTGRES_PASSWORD=test postgres:15
docker run -d --name test-redis redis:7-alpine

# 应用容器连接这些服务
docker run --rm \
  -e DATABASE_URL=postgres://user:test@test-db/mydb \
  -e REDIS_URL=redis://test-redis:6379 \
  myapp:latest npm run test:integration

# 清理
docker rm -f test-db test-redis

4. 分支环境预览

graph TB
    subgraph 分支环境自动化
        PR[Pull Request #42<br/>feature/new-api]
        CI_Docker[CI: docker build + push]
        DEPLOY[CI: 自动部署预览环境]
        PREVIEW[预览: pr-42.myapp.dev<br/>独立容器运行]
    end

    PR --> CI_Docker --> DEPLOY --> PREVIEW
    PREVIEW -->|PR 合并| CLEANUP[自动清理]

常用 CI/CD 工具中的 Docker

# GitHub Actions 示例
name: Docker CI/CD
on: [push]
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: test

    steps:
      - uses: actions/checkout@v3

      - name: Build Docker image
        run: docker build -t myapp .

      - name: Run tests
        run: docker run --rm myapp npm test

      - name: Push to registry
        run: |
          docker tag myapp ghcr.io/${{ github.repository }}:${{ github.sha }}
          docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

总结

Docker 在 CI/CD 中的核心价值可以概括为四个字:“环境即代码”

传统 CI/CD Docker CI/CD
“配环境”占 30% 时间 “写 Dockerfile”一次搞定
“在我机器上能跑” “在任何机器上都能跑”
开发/测试/生产三套环境 环境完全一致
交付 artifact(JAR/WAR) 交付镜像(完整的运行环境)
部署脚本复杂 docker run 一键部署

Docker 让 CI/CD 从”构建代码”变成了”构建运行环境”,这是 DevOps 理念的核心实践之一。


Docker 在 CI/CD 中的关键作用

Docker 在 CI/CD 中的关键作用

Docker 给 CI/CD 带来了什么?

Docker 通过”环境一致性”彻底改变了 CI/CD 流程。简单说就是:开发能用、测试能过、生产能跑的是同一个环境

无 Docker 的传统 CI/CD 痛点

graph TB
    subgraph 传统 CI/CD 痛点
        Dev[开发环境<br/>Python 3.10<br/>GCC 11<br/>Fedora 37]
        CI[CI 服务器<br/>Python 3.8<br/>GCC 9<br/>Ubuntu 20.04]
        Prod[生产环境<br/>Python 3.9<br/>GCC 10<br/>CentOS 7]
    end

    Dev -->|😱 这里能跑| CI
    CI -->|😰 这里报错| Prod

    style Dev fill:#fdd
    style CI fill:#fdd
    style Prod fill:#fdd

有 Docker 的 CI/CD 流程

graph LR
    subgraph 统一环境
        IMG[标准构建镜像<br/>Python 3.10 + GCC 11<br/>容器化]
    end

    subgraph 流水线
        Dev -->|git push| REPO[Git 仓库]
        REPO -->|触发| CIJenkins[CI/CD 服务器]
        CIJenkins -->|docker build| Build
        CIJenkins -->|docker run| Test
        CIJenkins -->|docker push| REG[镜像仓库]
    end

    IMG -.->|同一镜像| Build
    IMG -.->|同一镜像| Test
    IMG -.->|同一镜像| Prod

    REG -->|docker pull| Prod[生产环境]

    subgraph 生产环境
        Prod --> K8S[Kubernetes<br/>docker run]
    end

    style Dev fill:#dfd
    style Test fill:#dfd
    style Prod fill:#dfd

典型 Docker CI/CD 流水线

1. 代码推送触发构建

# .gitlab-ci.yml 示例
stages:
  - build
  - test
  - package
  - deploy

build:
  stage: build
  image: node:18
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/

test:
  stage: test
  image: node:18
  script:
    - npm ci
    - npm test
    - npm run lint

package:
  stage: package
  image: docker:latest
  services:
    - docker:dind  # Docker-in-Docker 支持
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

deploy:
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    - kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

2. Docker 镜像作为不可变交付物

# 构建一次,到处运行
# 构建阶段(CI)
docker build -t myapp:${BUILD_NUMBER} .
docker tag myapp:${BUILD_NUMBER} registry.example.com/myapp:${BUILD_NUMBER}
docker push registry.example.com/myapp:${BUILD_NUMBER}

# 测试阶段(用同一个镜像)
docker run registry.example.com/myapp:${BUILD_NUMBER} npm test

# 部署阶段(用同一个镜像)
docker pull registry.example.com/myapp:${BUILD_NUMBER}
docker run -d -p 80:80 registry.example.com/myapp:${BUILD_NUMBER}

Docker 在 CI/CD 中的关键作用

1. 构建环境标准化

# ci-build.Dockerfile — 统一构建环境
FROM node:18-alpine AS builder
RUN apk add --no-cache python3 make g++
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

2. 并行测试

# 利用容器隔离,并行运行不同版本的测试
docker run --rm myapp:latest "npm run test:unit" &
docker run --rm myapp:latest "npm run test:integration" &
docker run --rm myapp:latest "npm run test:e2e" &
wait

3. 环境变量和配置注入

# 在 CI 中启动依赖服务(数据库、缓存等)
docker run -d --name test-db -e POSTGRES_PASSWORD=test postgres:15
docker run -d --name test-redis redis:7-alpine

# 应用容器连接这些服务
docker run --rm \
  -e DATABASE_URL=postgres://user:test@test-db/mydb \
  -e REDIS_URL=redis://test-redis:6379 \
  myapp:latest npm run test:integration

# 清理
docker rm -f test-db test-redis

4. 分支环境预览

graph TB
    subgraph 分支环境自动化
        PR[Pull Request #42<br/>feature/new-api]
        CI_Docker[CI: docker build + push]
        DEPLOY[CI: 自动部署预览环境]
        PREVIEW[预览: pr-42.myapp.dev<br/>独立容器运行]
    end

    PR --> CI_Docker --> DEPLOY --> PREVIEW
    PREVIEW -->|PR 合并| CLEANUP[自动清理]

常用 CI/CD 工具中的 Docker

# GitHub Actions 示例
name: Docker CI/CD
on: [push]
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: test

    steps:
      - uses: actions/checkout@v3

      - name: Build Docker image
        run: docker build -t myapp .

      - name: Run tests
        run: docker run --rm myapp npm test

      - name: Push to registry
        run: |
          docker tag myapp ghcr.io/${{ github.repository }}:${{ github.sha }}
          docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

总结

Docker 在 CI/CD 中的核心价值可以概括为四个字:“环境即代码”

传统 CI/CD Docker CI/CD
“配环境”占 30% 时间 “写 Dockerfile”一次搞定
“在我机器上能跑” “在任何机器上都能跑”
开发/测试/生产三套环境 环境完全一致
交付 artifact(JAR/WAR) 交付镜像(完整的运行环境)
部署脚本复杂 docker run 一键部署

Docker 让 CI/CD 从”构建代码”变成了”构建运行环境”,这是 DevOps 理念的核心实践之一。


代码质量与工程化——从单元测试到 CI/CD 全链路

代码质量与工程化——从单元测试到 CI/CD 全链路

一、引言

代码质量是软件工程的永恒命题。良好的代码质量不仅意味着更少的 Bug,更意味着更低的维护成本、更快的交付速度和更顺畅的团队协作。然而,在追求交付速度的压力下,质量往往成为首当其冲的牺牲品。

工程化的核心思想是将软件开发从”手工作坊”升级为”工业流水线”——通过自动化工具、标准化流程和系统化的质量门禁,在保证效率的同时确保质量。本文将从单元测试出发,依次覆盖代码审查规范、Git 工作流和 CI/CD 流水线,最后介绍发布策略,帮助团队构建完整的质量保障体系。

二、单元测试

2.1 JUnit 5 核心特性

// JUnit 5 基础测试
@SpringBootTest
@AutoConfigureMockMvc
class OrderServiceTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private OrderRepository orderRepository;

    @InjectMocks
    @Autowired
    private OrderService orderService;

    private Order testOrder;

    @BeforeEach
    void setUp() {
        testOrder = new Order();
        testOrder.setId(1L);
        testOrder.setOrderNo("ORD20260516001");
        testOrder.setUserId(1001L);
        testOrder.setStatus("CREATED");
    }

    @Test
    @DisplayName("创建订单 - 成功场景")
    void testCreateOrder_Success() {
        when(orderRepository.save(any(Order.class)))
            .thenReturn(testOrder);

        Order result = orderService.createOrder(1001L, BigDecimal.valueOf(99.99));

        assertNotNull(result);
        assertEquals("ORD20260516001", result.getOrderNo());
        assertEquals("CREATED", result.getStatus());
        verify(orderRepository).save(any(Order.class));
    }

    @Test
    @DisplayName("取消订单 - 仅允许待支付状态")
    void testCancelOrder_OnlyPendingAllowed() {
        Order paidOrder = new Order();
        paidOrder.setId(2L);
        paidOrder.setStatus("PAID");
        when(orderRepository.findById(2L)).thenReturn(Optional.of(paidOrder));

        assertThrows(IllegalStateException.class, () -> {
            orderService.cancelOrder(2L);
        });
    }

    @ParameterizedTest
    @ValueSource(strings = {"CREATED", "PENDING_PAYMENT"})
    @DisplayName("取消订单 - 合法状态取消成功")
    void testCancelOrder_ValidStatus(String status) {
        testOrder.setStatus(status);
        when(orderRepository.findById(1L)).thenReturn(Optional.of(testOrder));

        orderService.cancelOrder(1L);

        assertEquals("CANCELLED", testOrder.getStatus());
    }
}

2.2 Mockito 高级用法

// 使用 ArgumentCaptor 捕获参数
@Test
void testSaveOrder_CaptureArgument() {
    orderService.createOrder(1001L, BigDecimal.valueOf(99.99));

    ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
    verify(orderRepository).save(captor.capture());

    Order saved = captor.getValue();
    assertThat(saved.getUserId()).isEqualTo(1001L);
    assertThat(saved.getTotalAmount()).isEqualByComparingTo("99.99");
}

// 验证调用顺序
@Test
void testOrderProcess_ExecutionOrder() {
    InOrder inOrder = inOrder(orderRepository, paymentClient);

    orderService.processOrder(1L);

    inOrder.verify(orderRepository).findById(1L);
    inOrder.verify(paymentClient).charge(any());
    inOrder.verify(orderRepository).save(any());
}

// doThrow / doAnswer
@Test
void testPaymentFailure_Compensation() {
    doThrow(new PaymentException("Insufficient funds"))
        .when(paymentClient).charge(any());

    assertThrows(PaymentException.class, () -> {
        orderService.processOrder(1L);
    });

    // 验证补偿操作
    verify(orderRepository).save(argThat(o -> o.getStatus().equals("CANCELLED")));
}

2.3 测试覆盖率

覆盖率类型 含义 目标
行覆盖率(Line) 代码行执行比例 ≥ 80%
分支覆盖率(Branch) 条件分支覆盖比例 ≥ 70%
方法覆盖率(Method) 方法被测试调用比例 ≥ 80%
路径覆盖率(Path) 所有可能路径覆盖 核心逻辑 ≥ 80%


    org.jacoco
    jacoco-maven-plugin
    0.8.11
    
        
            **/dto/**
            **/config/**
        
        
            
                CLASS
                
                    
                        LINE
                        COVEREDRATIO
                        0.80
                    
                
            
        
    
    
        
            prepare-agent
        
        
            report
            verify
            report
        
        
            check
            verify
            check
        
    

三、代码审查与静态分析

3.1 代码审查规范

审查清单

维度 检查项
正确性 业务逻辑是否正确?边界条件是否处理?
安全性 是否存在注入风险?敏感数据是否加密?
性能 是否存在 N+1 查询?是否创建了不必要的对象?
可读性 命名是否清晰?方法是否过长?注释是否必要?
可测试性 依赖是否可以 Mock?是否方便写单元测试?
可维护性 是否遵循单一职责?是否引入了不必要的复杂度?

3.2 SonarQube 静态分析

# sonar-project.properties
sonar.projectKey=order-service
sonar.projectName=Order Service
sonar.projectVersion=1.0
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.java.binaries=target/classes
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
sonar.exclusions=**/dto/**,**/config/**,**/*Application.java
sonar.qualitygate.wait=true

SonarQube 核心规则分类:

// Bug 检测
@PostConstruct  // ❌ @PostConstruct 在构造函数之前调用,其中依赖可能为 null
public void init() {
    this.defaultConfig = loadConfig("default.yaml");
}

// ✅ 正确方式
@PostConstruct
public void init() {
    if (configService != null) {  // 空指针防护
        this.defaultConfig = configService.loadConfig("default.yaml");
    }
}

// 代码异味——太长的 Lambda
public List<Order> findOrders() {
    return orderRepository.findAll().stream()
        .filter(o -> o.getStatus() != null && o.getStatus().equals("ACTIVE") 
            && o.getUserId() != null && o.getUserId() > 0)
        .collect(Collectors.toList());
}
// ✅ 提取方法
public List<Order> findOrders() {
    return orderRepository.findAll().stream()
        .filter(this::isActiveOrder)
        .collect(Collectors.toList());
}
private boolean isActiveOrder(Order o) {
    return o.getStatus() != null && o.getStatus().equals("ACTIVE") 
        && o.getUserId() != null && o.getUserId() > 0;
}

四、Git 工作流

4.1 Git Flow vs Trunk Based

graph TD
    subgraph GitFlow[Git Flow]
        M[master] --> R1[release/1.0]
        R1 --> M2[master v1.0 tag]
        D[develop] --> R1
        F1[feature/user-auth] --> D
        F2[feature/order-api] --> D
        H[hotfix/critical-bug] --> M
        M2 -.->|merge back| D
    end

    subgraph Trunk[Trunk Based]
        T[main] --> F3[feature/user-auth]
        F3 --> T
        T --> F4[feature/order-api]
        F4 --> T
        T1[main v1.0 tag]
    end
特性 Git Flow Trunk Based
分支数量 多(develop, release, feature, hotfix) 少(main + 短期 feature)
集成频率 每 Feature 分支集成一次 每日集成多次
发布周期 版本化发布 持续发布
适合团队 大型团队、版本化产品 小团队、SaaS 产品
冲突解决 集成时集中解决 风险小,及早暴露

4.2 分支命名规范

# Feature 分支
feature/user-auth-system   # 新功能
feature/JIRA-123-user-auth  # 关联 JIRA 工单

# Bugfix 分支
bugfix/order-null-pointer   # 非紧急 Bug
hotfix/critical-payment-fix # 紧急修复(从 master 分叉)

# 发布分支
release/1.2.3               # 准备发布

# 版本标签
git tag -a v1.2.3 -m "Release version 1.2.3"

4.3 提交信息规范

# Conventional Commits 规范
(): 

# 类型
feat:    新功能
fix:     Bug 修复
docs:    文档变更
style:   代码格式(无逻辑变更)
refactor:代码重构
perf:    性能优化
test:    添加测试
chore:   构建/工具变更
ci:      CI 配置变更

# 示例
feat(order): add order cancellation with refund support
fix(user): handle null email in password reset flow
refactor(payment): extract payment validation to separate service
perf(cache): reduce Redis TTL for hot product keys
test(order): add cancellation test for all statuses

五、CI/CD 流水线

5.1 GitHub Actions

name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: test
          MYSQL_DATABASE: testdb
        ports:
          - 3306:3306
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379

    steps:
    - uses: actions/checkout@v4

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

    - name: Run tests with coverage
      run: mvn clean verify -Pcoverage

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

    - name: SonarQube Scan
      uses: sonarsource/sonarqube-scan-action@master
      env:
        SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

  build:
    needs: test
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    - name: Build & Package
      run: mvn package -DskipTests -Pproduction

    - name: Build Docker image
      run: |
        docker build -t order-service:${{ github.sha }} .
        docker tag order-service:${{ github.sha }} \
          ghcr.io/${{ github.repository }}:${{ github.sha }}

    - name: Push to GitHub Container Registry
      run: |
        echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
        docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production

    steps:
    - name: Deploy to Kubernetes
      run: |
        kubectl set image deployment/order-service \
          order-service=ghcr.io/${{ github.repository }}:${{ github.sha }} \
          --namespace production

5.2 Jenkins Pipeline

pipeline {
    agent any

    environment {
        DOCKER_REGISTRY = 'registry.example.com'
        APP_NAME = 'order-service'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Unit Test') {
            steps {
                sh 'mvn test -Pcoverage'
            }
            post {
                success {
                    junit 'target/surefire-reports/*.xml'
                    jacoco classPattern: 'target/classes'
                }
            }
        }

        stage('SonarQube Analysis') {
            steps {
                withSonarQubeEnv('SonarQube') {
                    sh 'mvn sonar:sonar'
                }
            }
        }

        stage('Quality Gate') {
            steps {
                timeout(time: 5, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }

        stage('Build Docker Image') {
            steps {
                sh """
                    docker build -t ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} .
                    docker tag ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} \
                        ${DOCKER_REGISTRY}/${APP_NAME}:latest
                """
            }
        }

        stage('Deploy to Staging') {
            steps {
                sh 'kubectl apply -f k8s/staging/'
                sh 'kubectl rollout status deployment/${APP_NAME} -n staging'
            }
        }

        stage('Integration Test') {
            steps {
                sh 'mvn verify -Pintegration-test'
            }
        }

        stage('Deploy to Production') {
            input {
                message "Deploy to production?"
                ok "Proceed"
            }
            steps {
                sh 'kubectl apply -f k8s/production/'
                sh 'kubectl rollout status deployment/${APP_NAME} -n production'
            }
        }
    }

    post {
        success {
            emailext(
                subject: "Pipeline Success: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                body: "Deployment successful!",
                to: "team@example.com"
            )
        }
        failure {
            emailext(
                subject: "Pipeline Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                body: "Pipeline failed. Please check Jenkins.",
                to: "team@example.com"
            )
        }
    }
}

六、发布策略

6.1 策略对比

策略 描述 风险 复杂度
蓝绿部署 两套完整环境,切换流量
灰度发布 逐步增加新版本流量比例
金丝雀发布 少部分用户先用新版
滚动更新 逐个替换实例
# Kubernetes 滚动更新
apiVersion: apps/v1
kind: Deployment
spec:
  replicas: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2
      maxUnavailable: 1
  template:
    spec:
      containers:
      - name: order-service
        image: order-service:v2.0.0
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5

6.2 优雅关闭

@Configuration
public class GracefulShutdownConfig {

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.addConnectorCustomizers(connector -> {
            connector.setProperty("acceptCount", "100");
        });
        return factory;
    }
}
# Spring Boot 优雅关闭
server:
  shutdown: graceful  # 优雅关闭,等待请求处理完成
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s  # 最多等待 30 秒

七、总结

代码质量与工程化不是一个单一工具或流程能够解决的问题,而是一套完整的体系:

  1. 单元测试:JUnit 5 + Mockito + JaCoCo 构成了测试金字塔的底层基础,覆盖率达到 80% 是保障质量的基本门槛
  2. 代码审查:结合静态分析工具(SonarQube),在代码合并前自动发现缺陷和代码异味
  3. Git 工作流:Trunk Based 适合持续交付场景,Git Flow 适合版本化发布,团队应选择适合自身的模式
  4. CI/CD 流水线:每次代码提交都触发自动构建、测试和部署,将质量门禁自动化
  5. 发布策略:滚动更新、蓝绿部署、金丝雀发布在不同场景下各有优势

工程化的最终目标是:让交付高质量软件成为确定性流程,而非依赖个人英雄主义。当每次代码提交都能自动运行测试、分析质量、构建镜像、部署环境时,团队就可以将精力集中在真正的业务创新上。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容