1051 字
约 3 分钟
0
ArchUnit 入门:用单元测试守护分层架构

ArchUnit 入门:用单元测试守护分层架构

架构腐化不是一天发生的:某天有人图省事,Controller 直接调了 Mapper;三个月后,没人再说得清依赖规则——除了 ArchUnit,它把架构规则写成会失败的测试。

架构为什么会腐化

分层架构的规矩(Controller → Service → Mapper,禁止越层调用、禁止反向依赖)通常写在文档里、画在图上。但文档没有强制力:

  • 项目赶工期,有人发现 Controller 里直接调 Mapper「更快」,没人拦;
  • Code Review 没盯住一次越层调用,先例一开,后面人人效仿;
  • 一年后,分层图还是那张图,代码里越层调用已经无处不在——这就是架构腐化。

ArchUnit 的思路:把架构规则写成单元测试。规则违反 → 测试红 → CI 挂 → 合不进去。文档靠自觉,测试靠机器。

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit5</artifactId>
    <version>1.3.0</version>
    <scope>test</scope>
</dependency>

第一条规则:分层依赖

@AnalyzeClasses(packages = "com.clx.clarity", importOptions = ImportOption.DoNotIncludeTests.class)
class ArchitectureTest {

    @ArchTest
    static void 分层依赖必须自上而下(JavaClasses classes) {
        Architectures.layeredArchitecture().consideringAllDependencies()
            .layer("Controller").definedBy("..interfaces.admin..")
            .layer("Service").definedBy("..service..")
            .layer("Mapper").definedBy("..mapper..")
            .whereLayer("Controller").mayNotBeAccessedByAnyLayer()   // 没人能调 Controller
            .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
            .whereLayer("Mapper").mayOnlyBeAccessedByLayers("Service")
            .check(classes);
    }
}

这段测试声明了三件事:Controller 是最顶层没人能调它;Service 只能被 Controller 调;Mapper 只能被 Service 调。任何越层调用(哪怕一行),跑测试立刻爆出违反的类名和方法——精确到行。

常用规则示例

循环依赖检测:

@ArchTest
static void 不允许包之间循环依赖(JavaClasses classes) {
    NoClassesShould.dependOnClassesThat()
        .check(classes);   // 配 slices 检测包级循环
    SlicesRuleDefinition.slices().matching("com.clx.clarity.(**)..")
        .should().beFreeOfCycles().check(classes);
}

循环依赖是代码腐化最早的信号,人眼看不出来,ArchUnit 一查一个准。

注解约束:

@ArchTest
static void 写操作必须挂管理员权限(JavaClasses classes) {
    // 所有名含 save/update/delete 的公开方法必须有 @PreAuthorize
    methods().that().arePublic()
        .and().haveNameMatching(".*(save|update|delete).*")
        .should().beAnnotatedWith(PreAuthorize.class)
        .check(classes);
}

安全约束从「口口相传」变成了 CI 强制——这条规则的实践价值可能比分层检查还大。

命名与位置匹配:

@ArchTest
static void Service实现类必须放impl包(JavaClasses classes) {
    classes().that().haveSimpleNameEndingWith("ServiceImpl")
        .should().resideInAPackage("..service.impl..")
        .check(classes);
}

实际工程里的分寸

从宽松开始。给存量项目上 ArchUnit,第一步是先跑一次现状检查——大概率一堆红。此时不要硬写严格规则,而是:先把当前已违反的列出来,能低成本修的修掉,修不动又暂且合理的放进 freeze 存档(ArchUnit 支持「冻结已知违规,只拦新增」),让规则只对增量代码生效。约束增量比清算存量重要得多。

规则要有共识。架构测试的规则必须是团队真实认可的规矩——把没人遵守的「理想架构」写成测试,结果只有两个:要么测试永远红着没人管(比没有更糟),要么大家学会绕过。测试是共识的固化,不是共识的替代。

性能可接受。全量类分析在百级类的模块上是毫秒级,挂进常规 mvn test 无感;超大 monorepo 可单独一个 profile 隔离跑。

本站的实践

本站后端是六模块 Maven 多模块结构(web / admin / auth / search / ai / common),模块间依赖靠 ArchUnit 测试守着:

  • 模块边界:common 不准依赖任何业务模块(它是所有人的底层);
  • 分层:接口层不碰 Mapper、业务逻辑不进 Controller;
  • 规范:ServiceImpl 后缀、接口继承结构、写操作的权限注解。

一次想偷懒在 common 里引一个业务类的依赖,测试立刻红——那一刻就是 ArchUnit 的全部价值:腐化被拦在第一次发生时。

小结

ArchUnit 的核心洞察是:架构不是画出来的,是测试跑出来的。分层规则、循环依赖、注解约束,写进 JUnit 测试后,架构从「文档共识」升级为「机器强制」。落地口诀:从宽松规则起步、用 freeze 处理存量、只约束增量——让红测试保持权威性,规则就永远有效。

ArchUnit 入门:用单元测试守护分层架构
http://www.clxhxhhr.top/posts/3730/
作者
clxstart
发布于
2026-09-25
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。