Spring Boot 3.5 + Java 25 + Cloud Native 系列(七):架构质量Quality Gates
目录
在这个系列的前几篇文章中,我们看了 Shop Platform 的各个技术层面:API Gateway、BFF 聚合、领域服务、事件驱动、活动引擎。但有一个问题始终没有回答——如何保证 15+ 个服务、多个开发者在长时间演进中不把这些设计搞乱?
靠 Code Review 口头约定往往不够。人总是会犯错,尤其是当 deadline 临近的时候。按我目前的理解,一个更稳妥的办法是:把架构约束尽量变成可自动执行的测试用例。
📦 本文基于的完整项目源码:https://github.com/meirongdev/shop
上一篇:(六)插件化活动引擎
2026-04 实践更新 当前主线仓库里,ArchUnit、WireMock consumer contract tests、
@HttpExchange客户端基线、以及 JWKS 认证链都已经进入可验证状态;本文里的质量Quality Gates思路仍然适用,但哪些Quality Gates“只是方向”、哪些已经落地,请以 main 分支和docs/ROADMAP-2026.md为准。
质量Quality Gates全景#
Shop Platform 的架构质量保障由三个支柱组成:
┌─────────────────────────────────────────────────────────┐
│ 架构质量Quality Gates │
│ │
│ ① ArchUnit 规则 → 验证已有代码是否符合约定 │
│ (19 条规则) │
│ │
│ ② Maven Archetype → 新服务从标准化骨架出发 │
│ (6 个模板) │
│ │
│ ③ WireMock contract → BFF 与领域服务接口兼容 │
│ (13 个 contract tests) │
└─────────────────────────────────────────────────────────┘
ArchUnit:用代码验证架构#
ArchUnit 是一个 Java 库,可以在单元测试中验证类之间的依赖关系、命名约定、注解使用等架构约束。它的核心价值在于:把架构规则变成编译和 CI 中自动执行的测试,而不是文档里容易漂移的约定。
测试套件组织#
Shop Platform 的 ArchUnit 规则分布在 5 个测试类中,按关注点分类:
| 测试类 | 规则数量 | 关注点 |
|---|---|---|
ArchitectureRulesTest |
6 条 | 基础编码规范 + Kafka 幂等守护 |
CodingRulesTest |
7 条 | 编码规范扩展 |
LayeringRulesTest |
3 条 | 分层架构约束 |
NamingRulesTest |
2 条 | 命名规范 |
SpringRulesTest |
2 条 | Spring 特定架构约束 |
文档口径里按编号可以数到 20 项,但测试实现里 ARCH-03/04 合并为一条 NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS,所以当前真正执行的独立断言是 19 条。
所有规则都设置 .allowEmptyShould(true),这意味着当某个规则在当前代码库中没有匹配到类时不会报错。这对于渐进式引入规则非常重要——你可以在新模块中逐步遵守规则,而不会因为老代码不符合规则导致 CI 失败。
一、基础编码约定(ARCH-xx)#
ARCH-01:禁止 @Autowired 字段注入#
@ArchTest
static final ArchRule no_field_injection = fields()
.should().notBeAnnotatedWith(Autowired.class)
.because("Use constructor injection instead of @Autowired field injection; "
+ "prefer @RequiredArgsConstructor for conciseness")
.allowEmptyShould(true);
为什么:字段注入有三个致命问题:
- 依赖在编译期不可见,IDE 无法提示缺失的依赖
- 字段不能声明
final,失去了不可变性的编译期保障 - 单元测试往往需要借助反射或 Spring 上下文注入 mock
替代方案:构造器注入 + Lombok @RequiredArgsConstructor:
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository; // final + 构造器注入
private final IdempotencyGuard idempotencyGuard;
}
ARCH-02:禁止使用 RestTemplate#
@ArchTest
static final ArchRule no_rest_template = noClasses()
.should().dependOnClassesThat()
.haveFullyQualifiedName(RestTemplate.class.getName())
.because("RestTemplate is in maintenance mode; "
+ "use RestClient or @HttpExchange clients instead")
.allowEmptyShould(true);
为什么:RestTemplate 已进入维护模式,Spring 官方推荐使用 RestClient(同步)或 @HttpExchange(声明式)。两者都天然支持虚拟线程。
ARCH-03/04:禁止 System.out / System.err#
@ArchTest
static final ArchRule no_standard_streams =
NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS.allowEmptyShould(true);
为什么:直接写标准输出会绕过 Logback 配置,导致日志不携带 traceId、spanId、MDC 上下文。按当前项目的约定,日志更适合统一走 SLF4J。
ARCH-05:Kafka Listener 需要有幂等保护#
@ArchTest
static final ArchRule kafka_listeners_require_idempotency_guard_or_exemption =
methods()
.that().areAnnotatedWith(KafkaListener.class)
.should(beDeclaredInClassWithIdempotencyGuardOrExemption())
.because("Kafka listeners must use IdempotencyGuard "
+ "or document an equivalent mechanism via @IdempotencyExempt")
.allowEmptyShould(true);
这是最复杂的一条规则。它检查每个 @KafkaListener 方法所在的类:
- 要么注入了
IdempotencyGuard字段 - 要么标注了
@IdempotencyExempt注解
如果不满足任何一条,测试失败。
ARCH-06:IdempotencyGuard 调用者需要在 @Transactional 内运行#
@ArchTest
static final ArchRule idempotency_guard_callers_must_be_transactional =
methods()
.that(callIdempotencyGuardExecuteOnce())
.should().beAnnotatedWith(Transactional.class)
.because("IdempotencyGuard callers must run inside a @Transactional "
+ "so the idempotency key and business side-effects "
+ "are committed atomically")
.allowEmptyShould(true);
为什么:如果幂等键写入和业务操作不在同一事务中,可能出现:写入幂等键成功 → 业务操作失败 → 事务回滚但幂等键已残留 → 后续重试永远被判定为"已处理"。
二、编码约定扩展(CODE-xx)#
| 规则 | 禁止内容 | 替代方案 |
|---|---|---|
| CODE-01 | e.printStackTrace() |
log.error("msg", e) |
| CODE-02 | java.util.logging |
SLF4J + Logback |
| CODE-03 | Joda-Time | java.time (JSR-310) |
| CODE-04 | new ObjectMapper() 在 config 包外 |
注入 Spring 管理的 ObjectMapper |
| CODE-05 | Executors.newFixedThreadPool() 等传统线程池 |
newVirtualThreadPerTaskExecutor() 或 ThreadPoolTaskExecutor |
| CODE-06 | Google Gson | Jackson (Spring 默认) |
| CODE-07 | sun.* / com.sun.* 内部 API |
使用公开 API |
CODE-05 特别值得注意:它禁止了传统的线程池创建方式,但允许 Executors.newVirtualThreadPerTaskExecutor()。这通过检查方法调用名而非包名来实现:
private static DescribedPredicate<JavaMethodCall> callsLegacyExecutorFactory() {
return new DescribedPredicate<>("...") {
@Override
public boolean test(JavaMethodCall call) {
if (!call.getTarget().getOwner()
.isEquivalentTo(java.util.concurrent.Executors.class)) {
return false;
}
String name = call.getTarget().getName();
return name.equals("newFixedThreadPool")
|| name.equals("newCachedThreadPool")
|| name.equals("newSingleThreadExecutor")
|| name.equals("newScheduledThreadPool");
}
};
}
这种白名单方式保证了 Virtual Threads 不受限制,同时封堵了所有传统线程池的创建方式。
三、分层架构约束(LAYER-xx)#
LAYER-01:Service 层不得依赖 Controller 层#
@ArchTest
static final ArchRule services_must_not_depend_on_controllers = noClasses()
.that().resideInAnyPackage("..service..", "..engine..", "..index..")
.should().dependOnClassesThat().resideInAPackage("..controller..")
.because("Service/Engine/Index layers must not depend on the Controller layer; "
+ "keep business logic independent of the HTTP transport layer")
.allowEmptyShould(true);
为什么:Service 是纯业务逻辑层,不应该感知 HTTP 层的存在。如果 Service 需要返回 HTTP 特定对象(如 ResponseEntity),说明分层边界被破坏了。
LAYER-02:Controller 不得直接访问 Repository#
@ArchTest
static final ArchRule controllers_must_not_access_repositories = noClasses()
.that().resideInAPackage("..controller..")
.should().accessClassesThat()
.haveSimpleNameEndingWith("Repository")
.because("Controllers must access data through the Service layer, "
+ "not directly via *Repository")
.allowEmptyShould(true);
实际修复案例:MarketplaceInternalController 和 InternalActivityController 原先直接注入了 Repository,违反了这条规则。现已重构为通过 Service 层访问数据。
LAYER-03:顶层包之间不得有循环依赖#
@ArchTest
static final ArchRule no_package_cycles = slices()
.matching("dev.meirong.shop.(*)..")
.should().beFreeOfCycles()
.as("Top-level service packages must be free of cycles; "
+ "share contracts via shop-common and shop-contracts, "
+ "not direct cross-service imports")
.allowEmptyShould(true);
ArchUnit 的 slices() API 自动检测包之间的循环依赖。在微服务架构中,每个服务包应该保持单向依赖树,通过 shop-common 和 shop-contracts 共享,不得互相引用。
四、命名约定(NAME-xx)#
@ArchTest
static final ArchRule rest_controllers_named_correctly = classes()
.that().areAnnotatedWith(RestController.class)
.should().haveSimpleNameEndingWith("Controller")
.because("All @RestController classes must be named *Controller for consistency");
@ArchTest
static final ArchRule entities_in_domain_package = classes()
.that().areAnnotatedWith(Entity.class)
.should().resideInAPackage("..domain..")
.because("JPA @Entity classes must reside in the ..domain.. package");
两条规则都很基础,但对团队协作很有帮助:统一命名风格让新成员快速定位代码,*Controller 结尾和 domain 包约束也是团队最容易达成共识的规范。
五、Spring 特定约束(SPRING-xx)#
SPRING-01:BFF 不得持有 JPA Entity#
@ArchTest
static final ArchRule bff_must_not_have_jpa_entities = noClasses()
.that().resideInAnyPackage("..buyerbff..", "..sellerbff..")
.should().beAnnotatedWith(jakarta.persistence.Entity.class)
.because("BFF modules must not contain @Entity classes; "
+ "fetch data from Domain Services via HTTP")
.allowEmptyShould(true);
BFF 是聚合层,通过 HTTP 从领域服务获取数据。如果 BFF 有自己的 Entity,意味着它直接操作数据库——这打破了 BFF 的职责边界。
SPRING-02:shop-contracts 需要保持轻量#
@ArchTest
static final ArchRule contracts_must_be_lightweight = noClasses()
.that().resideInAPackage("dev.meirong.shop.contracts..")
.should().dependOnClassesThat().resideInAnyPackage(
"org.springframework.web.bind.annotation..",
"org.springframework.web.client..",
"org.springframework.data..",
"org.springframework.kafka..",
"jakarta.persistence..",
"org.springframework.stereotype..")
.because("shop-contracts must be a lightweight module "
+ "(DTOs + path constants + validation only); "
+ "adding Spring runtime to contracts forces those dependencies on all consumers")
.allowEmptyShould(true);
shop-contracts 是所有服务共享的contract模块。如果它引入了 Spring Web、Spring Data、Kafka 或 JPA 运行时依赖,这些依赖会强制扩散到所有消费方(BFF、Portal、Worker),造成框架耦合的连锁反应。
兼容性约定测试(非 ArchUnit)#
除了 ArchUnit 规则,CompatibilityConventionsTest 用标准 JUnit 验证 API contract规范:
@Test
void apiContracts_useVersionedBasePaths() {
// 所有 *Api 类的 BASE_PATH 必须包含 /v<版本号> 或 /internal
for (Class<?> apiType : importTopLevelClasses(API_PACKAGE)) {
if (!apiType.getSimpleName().endsWith("Api")) continue;
Field basePath = resolveBasePathField(apiType);
String value = (String) basePath.get(null);
if (!value.matches("^/.+/(v\\d+|internal)$")) {
violations.add(apiType.getName()
+ " BASE_PATH must end with /v<version> or /internal");
}
}
assertTrue(violations.isEmpty());
}
这保证了所有服务的 API 路径都是版本化的(如 /api/buyer/v1),裸路径(如 /api/buyer)不被允许。
同时验证事件contract的前向兼容:
@Test
void eventContracts_ignoreUnknownFields_andDefaultLegacyMetadata() {
// 所有事件数据必须标注 @JsonIgnoreProperties(ignoreUnknown = true)
// 并且 EventEnvelope 的 schemaVersion / contentType 有合理默认值
}
Maven Archetype:标准化服务脚手架#
ArchUnit 保证已有代码不腐化,而 Maven Archetype 保证新服务从一开始就符合规范。
六个 Archetype#
| Archetype | 适用场景 | 生成内容 |
|---|---|---|
gateway-service-archetype |
Spring Cloud Gateway 服务 | Gateway 路由配置、安全链、过滤器骨架 |
auth-service-archetype |
OAuth2 资源服务器 + JWT | Spring Security 配置、JWT 解析 |
bff-service-archetype |
BFF 聚合层 | RestClient 装配、ResilienceHelper 集成 |
domain-service-archetype |
JPA + Flyway + MySQL 领域服务 | Entity/Repository 骨架、Flyway V1 迁移 |
event-worker-archetype |
Kafka 消费者 + Outbox worker | Kafka Listener、Outbox Publisher |
portal-service-archetype |
Kotlin + Thymeleaf SSR 门户 | Kotlin 配置、Thymeleaf 模板 |
每个 Archetype 生成:
- 标准目录结构
pom.xml(继承父 POM,统一管理版本)application.yml(数据源、可观测、Actuator 预配置)- Application 主类
- 基础测试骨架
使用方式#
# 创建一个新的领域服务
mvn archetype:generate \
-DarchetypeGroupId=dev.meirong.shop \
-DarchetypeArtifactId=domain-service-archetype \
-DarchetypeVersion=0.1.0-SNAPSHOT \
-DgroupId=dev.meirong.shop \
-DartifactId=new-service \
-DserviceName=new
生成的服务开箱就有:
- 统一的包结构(
domain/、service/、controller/、config/) - Flyway V1 初始迁移脚本
- Testcontainers 测试基类
- Actuator + Micrometer 预配置
- OpenAPI SpringDoc 配置
Archetype 验证测试#
archetype-tests 模块对每个 Archetype 执行生成 + 编译 + 测试验证:
@Test
void shouldGenerateCompilableProject() throws Exception {
Path projectDir = generateProject();
compileProject(projectDir);
assertThat(projectDir.resolve("target/classes")).exists();
}
更底层的 AbstractArchetypeTest 还会先本地安装 shop-common、shop-contracts 和 shop-archetypes,再通过 Maven Invoker 运行 archetype:generate、compile、test。这保证了 Archetype 本身不会轻易“只剩模板、不再可用”。
WireMock contract testing#
ArchUnit 验证内部架构,WireMock 验证服务间的接口兼容性。
为什么需要 contract testing#
buyer-bff 调用 order-service 的 REST API,但两个服务独立开发、独立部署。如果 order-service 改了返回格式,buyer-bff 在运行时才会报错。
contract testing 的做法:用 WireMock mock order-service 的 HTTP 响应,验证 buyer-bff 能正确反序列化。
contract testing 示例#
// MarketplaceContractTest.java(无 Spring 上下文)
@Test
void deserializeMarketplaceProductResponse() {
// 启动 WireMock 服务器
WireMockServer wiremock = new WireMockServer(0);
wiremock.start();
// Mock marketplace-service 的返回
wiremock.stubFor(get(urlPathEqualTo("/api/marketplace/v1/products"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("""
{
"traceId": "trace-demo",
"status": "SC_OK",
"message": "Success",
"data": [{
"id": "prod-1",
"name": "Test Product",
"price": 29.99,
"inventory": 100
}]
}
""")));
// Mock ResilienceHelper passthrough
ResilienceHelper mockHelper = mock(ResilienceHelper.class);
when(mockHelper.read(anyString(), any(), any()))
.thenAnswer(inv -> inv.getArgument(1, Supplier.class).get());
// 验证 buyer-bff 能正确反序列化
var client = new MarketplaceClient(wiremock.baseUrl());
var products = client.listProducts();
assertThat(products).hasSize(1);
assertThat(products.get(0).name()).isEqualTo("Test Product");
}
contract testing 覆盖范围#
| 测试文件 | 验证的下游服务 |
|---|---|
MarketplaceContractTest |
marketplace-service 商品列表、详情 |
OrderContractTest |
order-service 订单列表、详情、取消 |
LoyaltyContractTest |
loyalty-service 积分账户、签到 |
SellerContractTest |
order-service、wallet-service、profile-service |
这些测试不启动 Spring 上下文(无 Redis、无数据库),执行速度极快(秒级),适合在 CI 中每次提交都运行。
更准确地说,这里做的是 BFF 侧的消费者兼容性校验:验证 buyer-bff / seller-bff 能否继续正确解析下游 JSON contract。它不是 Pact 这类"provider 也要回放验证"的完整 contract testing 平台,但对这个项目目前的演进速度来说,已经是高性价比的一层防线。
CI/CD 集成#
所有质量Quality Gates在 GitHub Actions CI 中自动执行:
# .github/workflows/ci.yml
jobs:
platform-validate:
steps:
- run: bash ./platform/scripts/validate-platform-assets.sh
maven-verify:
steps:
- run: ./mvnw -B -ntp verify
archetype-test:
steps:
- run: ./mvnw -pl shared/shop-common,shared/shop-contracts,tooling/shop-archetypes -am install -B -ntp
- run: ./mvnw -pl tooling/archetype-tests test -B -ntp
当前 CI 里和本文最相关的 job 主要有 4 个:platform-validate、maven-verify、archetype-test、docs-site。其中 maven-verify 负责跑 ArchUnit、contract testing 和普通单元测试,archetype-test 单独验证 6 个脚手架模板。任何一层失败都会阻断主分支集成。
零 catch(Exception) 原则#
项目追求的一个编码标准是:生产代码中不得有宽泛的 catch (Exception) 捕获。
宽泛的 catch 会吞掉所有异常,包括编程错误(NullPointerException、IllegalArgumentException),让调试变得极其困难。正确的做法是:
// ❌ 不好
try {
doSomething();
} catch (Exception e) {
log.error("Failed", e); // 吞掉了所有异常类型
}
// ✅ 好
try {
doSomething();
} catch (SpecificCheckedException e) {
log.error("Specific failure", e);
throw new BusinessException(CommonErrorCode.INTERNAL_ERROR, "...", e);
}
ArchUnit 目前没有直接 enforce 这条规则(通过 no_print_stack_trace 和 no_standard_streams 间接保证),但它是 Code Review 中的重点关注项。
参考与实现位置#
- ArchUnit User Guide:https://www.archunit.org/userguide/html/000_Index.html
- WireMock Docs:https://wiremock.org/docs/
- 仓库实现入口:
tooling/architecture-tests/src/test/java/dev/meirong/shop/architecture/、docs/ARCHUNIT-RULES.md、tooling/shop-archetypes/、tooling/archetype-tests/src/test/java/dev/meirong/shop/archetype/、services/buyer-bff/src/test/java/dev/meirong/shop/buyerbff/contract/、services/seller-bff/src/test/java/dev/meirong/shop/sellerbff/contract/SellerContractTest.java、.github/workflows/ci.yml
小结#
Shop Platform 的架构质量Quality Gates体系:
- 19 条 ArchUnit 规则:覆盖注入方式、HTTP 客户端选择、日志规范、线程模型、分层约束、命名规范、Kafka 幂等、BFF 边界、contract模块轻量等
- 兼容性约定测试:API 路径版本化强制校验、EventEnvelope 前向兼容验证
- 6 个 Maven Archetype:Gateway、Auth、BFF、Domain Service、Event Worker、Portal——新服务开箱就有标准结构和完整配置
- Archetype 验证测试:CI 中自动生成并编译,防止 Archetype 过期
- WireMock contract testing:无 Spring 上下文、秒级执行,验证 BFF 与领域服务的 JSON 反序列化兼容性
- CI 自动执行:
mvn verify阶段运行全部规则,违规阻断 PR 合并
系列总结#
本系列共七篇文章,完整覆盖了 Shop Platform 的技术架构:
| 篇目 | 主题 | 核心内容 |
|---|---|---|
| (一)总览 | 项目背景与整体架构 | 技术栈、服务清单、Virtual Threads 选型、Outbox Pattern 概述 |
| (二)API Gateway | 网关层 | Spring Cloud Gateway MVC、JWT 校验、Trusted Headers、Redis Lua 限流、灰度路由 |
| (三)BFF 聚合层 | 聚合层 | Virtual Threads 并发编排、Resilience4j 四层防护、结账 Saga 补偿、游客购物车 |
| (四)领域服务设计 | 业务核心 | 每服务独立数据库、Flyway 迁移、JPA 最佳实践、订单状态机、补偿任务 |
| (五)事件驱动架构 | 异步通信 | Kafka 话题矩阵、EventEnvelope、Transactional Outbox、IdempotencyGuard + Bloom Filter |
| (六)插件化活动引擎 | 营销活动 | GamePlugin SPI、4 种游戏插件、Redis Lua 抢红包、Anti-Cheat 反作弊 |
| (七)架构质量Quality Gates | 工程保障 | ArchUnit 19 条规则、6 个 Maven Archetype、WireMock contract testing |
项目仓库:github.com/meirongdev/shop(私有,仅供参考)