Quarkus 验证循环 是做什么的?
在 PR 之前、重大变更之后以及预部署时运行。
何时激活
- 在为一个 Quarkus 服务开启拉取请求之前
- 在重大重构或依赖项升级之后
- 为预发或生产环境进行预部署验证
- 运行完整构建 → 代码检查 → 测试 → 安全扫描 → 原生编译流水线
- 验证测试覆盖率是否满足阈值 (80%+)
- 测试原生镜像兼容性
第一阶段:构建
# Maven
mvn clean verify -DskipTests
# Gradle
./gradlew clean assemble -x test
如果构建失败,停止并修复编译错误。
第二阶段:静态分析
Checkstyle、PMD、SpotBugs (Maven)
mvn checkstyle:check pmd:check spotbugs:check
SonarQube (如果已配置)
mvn sonar:sonar \
-Dsonar.projectKey=my-quarkus-project \
-Dsonar.host.url=http://localhost:9000 \
-Dsonar.login=${SONAR_TOKEN}
需要解决的常见问题
- 未使用的导入或变量
- 复杂的方法(高循环复杂度)
- 潜在的空指针解引用
- SpotBugs 标记的安全问题
第三阶段:测试 + 覆盖率
# 运行所有测试
mvn clean test
# 生成覆盖率报告
mvn jacoco:report
# 强制执行覆盖率阈值 (80%)
mvn jacoco:check
# 或者使用 Gradle
./gradlew test jacocoTestReport jacocoTestCoverageVerification
测试类别
单元测试
使用模拟依赖项测试服务逻辑:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock UserRepository userRepository;
@InjectMocks UserService userService;
@Test
void createUser_validInput_returnsUser() {
var dto = new CreateUserDto("Alice", "[email protected]");
// Panache persist() 是 void 类型 — 使用 doNothing + verify
doNothing().when(userRepository).persist(any(User.class));
User result = userService.create(dto);
assertThat(result.name).isEqualTo("Alice");
verify(userRepository).persist(any(User.class));
}
}
集成测试
使用真实数据库进行测试 (Testcontainers):
@QuarkusTest
@QuarkusTestResource(PostgresTestResource.class)
class UserRepositoryIntegrationTest {
@Inject
UserRepository userRepository;
@Test
@Transactional
void findByEmail_existingUser_returnsUser() {
User user = new User();
user.name = "Alice";
user.email = "[email protected]";
userRepository.persist(user);
Optional<User> found = userRepository.findByEmail("[email protected]");
assertThat(found).isPresent();
assertThat(found.get().name).isEqualTo("Alice");
}
}
API 测试
使用 REST Assured 测试 REST 端点:
@QuarkusTest
class UserResourceTest {
@Test
void createUser_validInput_returns201() {
given()
.contentType(ContentType.JSON)
.body("""
{"name": "Alice", "email": "[email protected]"}
""")
.when().post("/api/users")
.then()
.statusCode(201)
.body("name", equalTo("Alice"));
}
@Test
void createUser_invalidEmail_returns400() {
given()
.contentType(ContentType.JSON)
.body("""
{"name": "Alice", "email": "invalid"}
""")
.when().post("/api/users")
.then()
.statusCode(400);
}
}
覆盖率报告
查看 target/site/jacoco/index.html 获取详细覆盖率:
- 总体代码行覆盖率(目标:80%+)
- 分支覆盖率(目标:70%+)
- 识别未覆盖的关键路径
第四阶段:安全扫描
依赖项漏洞 (Maven)
mvn org.owasp:dependency-check-maven:check
查看 target/dependency-check-report.html 中的 CVE。
Quarkus 安全审计
# 检查易受攻击的扩展
mvn quarkus:audit
# 列出所有扩展
mvn quarkus:list-extensions
OWASP ZAP (API 安全测试)
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-api-scan.py \
-t http://localhost:8080/q/openapi \
-f openapi
常见安全检查
- 所有密钥存储在环境变量中(不在代码中)
- 对所有端点进行输入验证
- 已配置身份验证/授权
- 正确配置了 CORS
- 设置了安全头
- 密码使用 BCrypt 哈希
- SQL 注入保护(参数化查询)
- 对公共端点进行速率限制
第五阶段:原生编译
测试 GraalVM 原生镜像兼容性:
# 构建原生可执行文件
mvn package -Dnative
# 或者使用容器
mvn package -Dnative -Dquarkus.native.container-build=true
# 测试原生可执行文件
./target/*-runner
# 运行基本冒烟测试
curl http://localhost:8080/q/health/live
curl http://localhost:8080/q/health/ready
原生镜像故障排除
常见问题:
- 反射:为动态类添加反射配置
- 资源:使用
quarkus.native.resources.includes包含资源 - JNI:如果使用原生库,注册 JNI 类
反射配置示例:
@RegisterForReflection(targets = {MyDynamicClass.class})
public class ReflectionConfiguration {}
第六阶段:性能测试
使用 K6 进行负载测试
// load-test.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 50 },
{ duration: '1m', target: 100 },
{ duration: '30s', target: 0 },
],
};
export default function () {
const res = http.get('http://localhost:8080/api/markets');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
});
}
运行:
k6 run load-test.js
需要监控的指标
- 响应时间 (p50, p95, p99)
- 吞吐量 (每秒请求数)
- 错误率
- 内存使用量
- CPU 使用率
第七阶段:健康检查
# 存活检查
curl http://localhost:8080/q/health/live
# 就绪检查
curl http://localhost:8080/q/health/ready
# 所有健康检查
curl http://localhost:8080/q/health
# 指标(如果启用)
curl http://localhost:8080/q/metrics
预期响应:
{
"status": "UP",
"checks": [
{
"name": "Database connection",
"status": "UP"
}
]
}
第八阶段:容器镜像构建
# 构建容器镜像
mvn package -Dquarkus.container-image.build=true
# 或使用特定仓库
mvn package \
-Dquarkus.container-image.build=true \
-Dquarkus.container-image.registry=docker.io \
-Dquarkus.container-image.group=myorg \
-Dquarkus.container-image.tag=1.0.0
# 测试容器
docker run -p 8080:8080 myorg/my-quarkus-app:1.0.0
容器安全扫描
# Trivy
trivy image myorg/my-quarkus-app:1.0.0
# Grype
grype myorg/my-quarkus-app:1.0.0
第九阶段:配置验证
# 检查所有配置属性
mvn quarkus:info
# 列出所有配置源
curl http://localhost:8080/q/dev/io.quarkus.quarkus-vertx-http/config
环境特定检查
- 数据库 URL 按环境配置
- 密钥外部化(Vault、环境变量)
- 日志级别设置适当
- CORS 源设置正确
- 已配置速率限制
- 已启用监控/追踪
第十阶段:文档审查
- OpenAPI/Swagger 文档是最新的 (
/q/swagger-ui) - README 包含设置说明
- API 变更已记录
- 破坏性变更的迁移指南
- 配置属性已记录
生成 OpenAPI 规范:
curl http://localhost:8080/q/openapi -o openapi.json
验证清单
代码质量
- 构建通过且无警告
- 静态分析干净(无高/中优先级问题)
- 代码遵循团队约定
- PR 中没有注释掉的代码或 TODO
测试
- 所有测试通过
- 代码覆盖率 ≥ 80%
- 使用真实数据库进行集成测试
- 安全测试通过
- 性能在可接受范围内
安全
- 没有依赖漏洞
- 已验证身份验证/授权
- 输入验证完成
- 密钥不在源代码中
- 已配置安全头
部署
- 原生编译成功
- 容器镜像构建成功
- 健康检查正确响应
- 配置对目标环境有效
原生镜像
- 原生可执行文件构建成功
- 原生测试通过
- 启动时间 < 100ms
- 内存占用可接受
自动化验证脚本
#!/bin/bash
set -e
echo "=== 第一阶段:构建 ==="
mvn clean verify -DskipTests
echo "=== 第二阶段:静态分析 ==="
mvn checkstyle:check pmd:check spotbugs:check
echo "=== 第三阶段:测试 + 覆盖率 ==="
mvn test jacoco:report jacoco:check
echo "=== 第四阶段:安全扫描 ==="
mvn org.owasp:dependency-check-maven:check
echo "=== 第五阶段:原生编译 ==="
mvn package -Dnative -Dquarkus.native.container-build=true
echo "=== 所有阶段完成 ==="
echo "审查报告:"
echo " - 覆盖率:target/site/jacoco/index.html"
echo " - 安全:target/dependency-check-report.html"
echo " - 原生:target/*-runner"
CI/CD 集成
GitHub Actions 示例
name: Verification
on: [push, pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Cache Maven packages
uses: actions/cache@v6
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
- name: Build
run: mvn clean verify -DskipTests
- name: Test with Coverage
run: mvn test jacoco:report jacoco:check
- name: Security Scan
run: mvn org.owasp:dependency-check-maven:check
- name: Upload Coverage
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: target/site/jacoco/jacoco.xml
最佳实践
- 在每次 PR 前运行验证循环
- 在 CI/CD 流水线中自动化
- 立即修复问题;不要累积技术债务
- 保持覆盖率在 80% 以上
- 定期更新依赖项
- 定期测试原生编译
- 监控性能趋势
- 记录破坏性变更
- 审查安全扫描结果
- 为每个环境验证配置