你以为“分层”靠自觉就够了?——ArchUnit 让架构违规在 CI 里当场社死!
🏆本文收录于《滚雪球学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.OrderControllercom.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 现实建议:从粗到细,别一上来就“切太碎”
我一般是:
- 先按一级模块(比如
..myapp.(*)..)查大环 - 再对问题模块局部加细粒度规则(比如
..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 官方用户指南给出的规则模型与示例(分层、切片无环、命名/包约束)。
结尾:你要的不是“优雅”,是“长期不崩”😄
架构这东西,最怕两种人:
- **“我就这一次”**的人(一次一次就成传统了)
- **“之后再重构”**的人(之后永远不会来)
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 -
- 点赞
- 收藏
- 关注作者
评论(0)