📌 本文由 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 秒
七、总结
代码质量与工程化不是一个单一工具或流程能够解决的问题,而是一套完整的体系:
- 单元测试:JUnit 5 + Mockito + JaCoCo 构成了测试金字塔的底层基础,覆盖率达到 80% 是保障质量的基本门槛
- 代码审查:结合静态分析工具(SonarQube),在代码合并前自动发现缺陷和代码异味
- Git 工作流:Trunk Based 适合持续交付场景,Git Flow 适合版本化发布,团队应选择适合自身的模式
- CI/CD 流水线:每次代码提交都触发自动构建、测试和部署,将质量门禁自动化
- 发布策略:滚动更新、蓝绿部署、金丝雀发布在不同场景下各有优势
工程化的最终目标是:让交付高质量软件成为确定性流程,而非依赖个人英雄主义。当每次代码提交都能自动运行测试、分析质量、构建镜像、部署环境时,团队就可以将精力集中在真正的业务创新上。
代码质量与工程化——从单元测试到 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 秒
七、总结
代码质量与工程化不是一个单一工具或流程能够解决的问题,而是一套完整的体系:
- 单元测试:JUnit 5 + Mockito + JaCoCo 构成了测试金字塔的底层基础,覆盖率达到 80% 是保障质量的基本门槛
- 代码审查:结合静态分析工具(SonarQube),在代码合并前自动发现缺陷和代码异味
- Git 工作流:Trunk Based 适合持续交付场景,Git Flow 适合版本化发布,团队应选择适合自身的模式
- CI/CD 流水线:每次代码提交都触发自动构建、测试和部署,将质量门禁自动化
- 发布策略:滚动更新、蓝绿部署、金丝雀发布在不同场景下各有优势
工程化的最终目标是:让交付高质量软件成为确定性流程,而非依赖个人英雄主义。当每次代码提交都能自动运行测试、分析质量、构建镜像、部署环境时,团队就可以将精力集中在真正的业务创新上。
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
最佳实践
- 基础镜像最小化:使用
alpine或distroless减少攻击面 - 定期扫描:不只是 CI 构建时扫描,基础镜像更新后也要重新扫描
- CVE 白名单:部分 CVE(如内核相关)不影响容器运行时,可设白名单
- 多阶段构建:最终运行镜像只包含运行所需的最小文件
- 签名验证:使用
cosign对镜像签名,防止被篡改
面试要点
- Trivy 是目前最流行的开源容器安全扫描工具
- 扫描应放在 CI 的构建阶段,阻止高危镜像进入仓库
- 不仅要扫应用层,基础镜像层也要定期扫描
- 最小化基础镜像 + 及时更新 = 最佳安全组合
- 镜像签名和准入控制是企业级安全的关键
面试官常问:如果扫描发现一个高危漏洞但不影响你的业务,怎么处理?
金丝雀发布
金丝雀发布
什么是金丝雀发布
金丝雀发布(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 测试 | 天然支持 | 不支持 | 不支持 |
面试要点
- 金丝雀发布是最安全的发布策略之一,适合重大版本变更
- Docker 环境结合 Nginx/Traefik 可以实现灵活的流量控制
- 关键在于监控指标的准确性——如果指标不准,金丝雀就失去了意义
- 自动回滚策略:监控到错误率上升后自动切换回旧版本
- 数据兼容性:新旧版本必须能同时访问同一数据源
面试官常问:金丝雀发布你们用了哪些灰度策略?如果发现新版本有问题怎么处理?
蓝绿部署与滚动部署
蓝绿部署与滚动部署
蓝绿部署(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(额外一个) |
| 切换速度 | 极快(瞬间切换) | 较慢(逐个替换) |
| 回滚速度 | 极快(切回即可) | 较慢(逐个回滚) |
| 流量影响 | 无(瞬时切换) | 平滑过渡 |
| 成本 | 高 | 低 |
| 适用场景 | 关键服务、数据库变更 | 无状态服务 |
面试要点
- 蓝绿部署需要两倍资源,但切换和回滚都是瞬间完成的
- 滚动部署资源消耗小,适合无状态微服务
- 蓝绿部署要做数据兼容性处理(新旧代码可能同时访问同一数据库)
- Docker Swarm 原生支持滚动更新
- 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 .
面试要点
- GitLab CI 通过 DinD 服务实现容器内的 Docker 构建
- GitLab Container Registry 提供私有镜像仓库能力
- 使用
CI_JOB_TOKEN实现安全的镜像推送 - 多架构构建需要 Buildx + QEMU
- 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 |
自动可用,无需设置 |
面试要点
- GitHub Actions 提供标准的 Docker Action 简化构建流程
docker/build-push-action支持多平台、缓存、推送一站式- GHCR 是 Docker Hub 的优秀替代方案(免费、无拉取限制)
- 缓存使用
type=gha是 GitHub Actions 的最佳实践 - 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
面试要点
- Docker 解决了 CI/CD 中最头疼的环境一致性问题
- 构建、测试、部署三个环节都受益于容器化
- 每提交一次代码都生成一个不可变镜像,版本可追溯
- Docker Compose 在集成测试中极其实用
- 配合镜像仓库(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
# 如果未签名会失败
扫描策略的最佳实践
- 在 CI 中自动扫描:每次构建都扫描
- 设置阈值:阻止含有 CRITICAL 级别漏洞的镜像部署
- 定期重新扫描:新漏洞不断发现
- 最小化基础镜像:减少攻击面
- 跟踪修复:记录漏洞的修复状态
- 扫描运行时容器:不只是镜像,运行中的容器也需要扫描
# 运行时容器扫描
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 秒
七、总结
代码质量与工程化不是一个单一工具或流程能够解决的问题,而是一套完整的体系:
- 单元测试:JUnit 5 + Mockito + JaCoCo 构成了测试金字塔的底层基础,覆盖率达到 80% 是保障质量的基本门槛
- 代码审查:结合静态分析工具(SonarQube),在代码合并前自动发现缺陷和代码异味
- Git 工作流:Trunk Based 适合持续交付场景,Git Flow 适合版本化发布,团队应选择适合自身的模式
- CI/CD 流水线:每次代码提交都触发自动构建、测试和部署,将质量门禁自动化
- 发布策略:滚动更新、蓝绿部署、金丝雀发布在不同场景下各有优势
工程化的最终目标是:让交付高质量软件成为确定性流程,而非依赖个人英雄主义。当每次代码提交都能自动运行测试、分析质量、构建镜像、部署环境时,团队就可以将精力集中在真正的业务创新上。


暂无评论内容