在这个系列的前几篇文章中,我们看了 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);

为什么:字段注入有三个致命问题:

  1. 依赖在编译期不可见,IDE 无法提示缺失的依赖
  2. 字段不能声明 final,失去了不可变性的编译期保障
  3. 单元测试往往需要借助反射或 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)#

flowchart LR C["Controller"] -->|"调用"| S["Service / Engine / Index"] S -->|"调用"| D["Domain (Entity + Repository)"] S -.->|"❌ 禁止反向依赖"| C D -.->|"❌ 禁止反向依赖"| S

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);

实际修复案例MarketplaceInternalControllerInternalActivityController 原先直接注入了 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-commonshop-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-commonshop-contractsshop-archetypes,再通过 Maven Invoker 运行 archetype:generatecompiletest。这保证了 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-validatemaven-verifyarchetype-testdocs-site。其中 maven-verify 负责跑 ArchUnit、contract testing 和普通单元测试,archetype-test 单独验证 6 个脚手架模板。任何一层失败都会阻断主分支集成。


零 catch(Exception) 原则#

项目追求的一个编码标准是:生产代码中不得有宽泛的 catch (Exception) 捕获

宽泛的 catch 会吞掉所有异常,包括编程错误(NullPointerExceptionIllegalArgumentException),让调试变得极其困难。正确的做法是:

// ❌ 不好
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_traceno_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.mdtooling/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(私有,仅供参考)