你以为“分层”靠自觉就够了?——ArchUnit 让架构违规在 CI 里当场社死!

举报
bug菌 发表于 2026/01/13 11:47:55 2026/01/13
【摘要】 🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。  本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。...

🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
  
本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】https://blog.csdn.net/weixin_43970743/article/details/151115907,你想学习的都被收集在内,快速投入学习!!两不误。
  
若还想学习更多,可直接前往《滚雪球学SpringBoot(全版本合集)》:https://blog.csdn.net/weixin_43970743/category_11599389.html,涵盖SpringBoot所有版本教学文章。

演示环境说明:

  • 开发工具:IDEA 2021.3
  • JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
  • Spring Boot版本:3.5.4(于25年7月24日发布)
  • Maven版本:3.8.2 (或更高)
  • Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
  • 操作系统:Windows 11

前言:靠“约定”守架构?我劝你别太乐观🤣

讲真,我见过太多项目的分层规范写得像诗:

  • Controller 只做参数校验和转发
  • Service 承载业务编排
  • DAO/Repository 只管数据访问

然后一转身,现实给你一记闷棍:

  • 某个 Controller 为了“省事”,直接注入 Repository;
  • 某个 DTO 被 Service、Controller、DAO 反复互相引用;
  • 包之间绕来绕去,最后形成循环依赖,改个类像拆炸弹🧨

这时候你再发群消息提醒“大家注意分层”,说实话,效果跟对空气讲道理差不多……
所以我更愿意把架构规范变成一种可执行的测试:只要有人破坏规则,单测直接红灯,CI 直接拦住——别争了,机器更公平😄

ArchUnit 的定位正是:通过分析 Java 字节码,把架构/依赖/循环等规则写成单元测试来自动验证。

1. 为什么需要“代码架构测试”?(不是矫情,是止损🙂)

1.1 架构违规的特点:隐蔽、扩散、难回滚

架构问题最讨厌的地方在于:

  • 不会立刻报错,但会慢慢把代码变成一坨“胶水”;
  • 你发现的时候通常已经扩散:A 依赖 B,B 依赖 C,最后 C 还反过来依赖 A;
  • 修起来又容易牵一发动全身,尤其是多人协作时,谁动谁挨骂😅

1.2 静态扫描 vs 架构单测:一个“原则”,一个“底线”

Sonar 这类工具偏“通用规则”,但你项目的“包结构约束 / 命名规范 / 分层访问规则”往往是业务团队特有的
ArchUnit 的强项就是:你能用 DSL 把“我们团队就这么干”的规矩写成测试规则,并用任何单元测试框架跑起来。

2. ArchUnit 怎么接入?(JUnit 5 写法,少废话😄)

ArchUnit 官方用户指南里给了 JUnit 5 的依赖与测试支持方式:用 @AnalyzeClasses 声明要扫描的包,再用 @ArchTest 定义规则字段,测试引擎会自动导入并执行。

2.1 Maven 依赖(示例)

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

版本号以你项目实际为准,这里只是示例;ArchUnit 的 JUnit 5 便利包是官方推荐接入方式之一。

2.2 测试骨架(JUnit 5)

import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;

import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;

@AnalyzeClasses(packages = "com.example.myapp")
public class ArchitectureTest {
    // @ArchTest 放规则
}

3. Layered Architecture Check:强制 Controller 不能直接调 DAO

你大纲里这条我太懂了:Controller 直连 DAO 的诱惑简直像深夜外卖——方便是真的方便,后患也是真的后患😇

ArchUnit 的用户指南给了一个非常经典的分层规则 DSL:定义 Controller / Service / Persistence 三层,并约束访问关系:Persistence 只能被 Service 访问,Service 只能被 Controller 访问,而 Controller 不允许被其他层访问。

3.1 分层规则(直接可用)

import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.library.Architectures.layeredArchitecture;

public class ArchitectureTest {

    @ArchTest
    static final ArchRule layered_architecture_should_be_respected =
            layeredArchitecture()
                    .consideringAllDependencies()
                    .layer("Controller").definedBy("..controller..")
                    .layer("Service").definedBy("..service..")
                    .layer("Persistence").definedBy("..persistence..")

                    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
                    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
                    .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service");
}

3.2 这条规则怎么“拦截”Controller → DAO?

假设你有:

  • com.example.myapp.controller.OrderController
  • com.example.myapp.persistence.OrderRepository

如果 OrderController 直接注入并调用 OrderRepository,就意味着:

Controller 访问了 Persistence
而规则明确规定:Persistence 只能被 Service 访问。
所以测试会直接失败,并给出“违规依赖链”。

这时候你不用吵架:

  • “我这次只是图方便!”
  • “我这次是临时方案!”
    在 CI 面前都没用——要么补 Service,要么解释为什么要改规则。规矩就是规矩🙂

4. Cyclic Dependency Check:自动检测包循环依赖(别等它长大😵‍💫)

循环依赖这东西,特别像房间角落里的灰:

  • 一开始一点点,你懒得管;
  • 过几个月,谁动都呛;
  • 到最后,重构像拆迁😅

ArchUnit 用户指南直接给了“切片(Slices)+ 无环(beFreeOfCycles)”的写法:
slices().matching("com.myapp.(*)..").should().beFreeOfCycles()

4.1 按“一级子包”切片检测循环

import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;

public class ArchitectureTest {

    @ArchTest
    static final ArchRule no_cycles_between_slices =
            slices()
                    .matching("com.example.myapp.(*)..")
                    .should()
                    .beFreeOfCycles();
}

它在干嘛?

  • matching("..(*)&..") 会把包按捕获组切成 slice
  • 然后检查这些 slice 之间是否存在依赖环

用户指南还补充了更细的匹配方式:

  • ..myapp.(*)..:取 myapp 后第一段包名作为 slice
  • ..myapp.(**):取所有子包作为 slice 来检测

4.2 现实建议:从粗到细,别一上来就“切太碎”

我一般是:

  1. 先按一级模块(比如 ..myapp.(*)..)查大环
  2. 再对问题模块局部加细粒度规则(比如 ..myapp.(**).service..

因为规则太细会导致“测试报一屏”,新人看完只想把 ArchUnit 删掉(别问我怎么知道的🤣)。

5. Naming Convention Check:把命名规范写成“硬约束”(不靠口头约定)

命名规范听起来小儿科,但它对可读性影响巨大。更要命的是:命名一旦乱了,架构边界就容易跟着乱

ArchUnit 的用户指南里就给了不少“命名与结构”的规则示例,比如:

  • classes().that().haveSimpleNameStartingWith("Foo").should().resideInAPackage("com.foo")(名字与包结构一致)
  • classes().that().implement(Connection.class).should().haveSimpleNameEndingWith("Connection")(实现某接口必须以固定后缀命名)
    这些例子非常适合改造成团队自己的命名约束。

下面我给你三种最常用、最“落地”的写法。

5.1 强制 Controller 类必须以 Controller 结尾,且只能在 controller 包

import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;

public class ArchitectureTest {

    @ArchTest
    static final ArchRule controllers_should_be_named_and_located_consistently =
            classes()
                    .that().resideInAPackage("..controller..")
                    .should().haveSimpleNameEndingWith("Controller");
}

你也可以反过来写:名字像 Controller 的类必须在 controller 包里:

@ArchTest
static final ArchRule classes_named_controller_should_reside_in_controller_package =
        classes()
                .that().haveSimpleNameEndingWith("Controller")
                .should().resideInAPackage("..controller..");

这种“命名 ↔ 包结构”的绑定,在用户指南的示例里属于典型用法。

5.2 强制 Repository/DAO 接口命名规范(比如必须以 Repository 结尾)

@ArchTest
static final ArchRule repositories_should_have_proper_suffix =
        classes()
                .that().resideInAPackage("..persistence..")
                .and().areInterfaces()
                .should().haveSimpleNameEndingWith("Repository");

这条规则的价值在于:当你看到 UserRepository,你不用点进去就知道它大概率是持久层接口。
团队协作里,这种“心智成本下降”真的很值钱🙂

5.3 强制 Service 类必须以 Service 结尾,且不能被 controller 包以外直接 new(可选进阶)

先做简单命名:

@ArchTest
static final ArchRule services_should_be_named_service =
        classes()
                .that().resideInAPackage("..service..")
                .should().haveSimpleNameEndingWith("Service");

再做一个“更像工程规矩”的限制:Service 不允许在 controller 里被直接实例化(防止 new XxxService() 这种野路子),你可以用依赖规则来做——ArchUnit 本质是分析字节码依赖,它擅长这个。

6. 把三类规则放一起:一份能直接跑的“架构守护者”测试类✅

import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;

@AnalyzeClasses(packages = "com.example.myapp")
public class ArchitectureTest {

    // 1) 分层:Controller -> Service -> Persistence
    @ArchTest
    static final ArchRule layered_architecture_should_be_respected =
            layeredArchitecture()
                    .consideringAllDependencies()
                    .layer("Controller").definedBy("..controller..")
                    .layer("Service").definedBy("..service..")
                    .layer("Persistence").definedBy("..persistence..")
                    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
                    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
                    .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service");

    // 2) 环依赖:按一级模块切片查 cycles
    @ArchTest
    static final ArchRule no_cycles_between_modules =
            slices()
                    .matching("com.example.myapp.(*)..")
                    .should().beFreeOfCycles();

    // 3) 命名规范:controller 包内类名必须以 Controller 结尾
    @ArchTest
    static final ArchRule controllers_should_be_named_controller =
            classes()
                    .that().resideInAPackage("..controller..")
                    .should().haveSimpleNameEndingWith("Controller");
}

这些 DSL 都来自 ArchUnit 官方用户指南给出的规则模型与示例(分层、切片无环、命名/包约束)。

结尾:你要的不是“优雅”,是“长期不崩”😄

架构这东西,最怕两种人:

  1. **“我就这一次”**的人(一次一次就成传统了)
  2. **“之后再重构”**的人(之后永远不会来)

ArchUnit 的厉害之处是:它把“规范”从文档里拎出来,变成你 CI 里跑得动、拦得住的测试。它不讲情面,但它讲道理——说白了,它就是你团队里那个“不好说话但很靠谱”的架构保安🛡️。

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。

ps:本文涉及所有源代码,均已上传至Gitee:https://gitee.com/bugjun01/SpringBoot-demo 开源,供同学们一对一参考 Gitee传送门https://gitee.com/bugjun01/SpringBoot-demo,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗

🫵 Who am I?

我是 bug菌:

  • 热活跃于 CSDN:https://blog.csdn.net/weixin_43970743 | 掘金:https://juejin.cn/user/695333581765240 | InfoQ:https://www.infoq.cn/profile/4F581734D60B28/publish | 51CTO:https://blog.51cto.com/u_15700751 | 华为云:https://bbs.huaweicloud.cn/community/usersnew/id_1582617489455371 | 阿里云:https://developer.aliyun.com/profile/uolxikq5k3gke | 腾讯云:https://cloud.tencent.com/developer/user/10216480/articles 等技术社区;
  • CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
  • 掘金、InfoQ、51CTO 等平台签约及优质作者;
  • 全网粉丝累计 30w+

更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看:https://bbs.csdn.net/topics/612438251 👈️
硬核技术公众号 「猿圈奇妙屋」https://bbs.csdn.net/topics/612438251 期待你的加入,一起进阶、一起打怪升级。

- End -

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。