前言
单体项目随着业务增长,代码臃肿、模块间横向调用、边界模糊等问题会逐渐显现。Maven 多模块架构是应对这一问题的经典方案:强制物理边界,让依赖关系可见、可控。
但仅靠目录拆分远远不够——没人能保证工程师不会在 controller 里直接注入另一个模块的 Mapper。ArchUnit 正是解决这一问题的工具:将架构规则编写成测试,在每次 mvn test 时自动校验,一旦有人违反分层约束,CI 直接失败。
本文结合 personal-blog-backend 项目,完整讲解:
- Maven 多模块的结构设计与依赖管理
-api/-service的分层边界原则- ArchUnit 如何将架构规则变成可执行的测试
- 实际运行效果与踩坑记录
本文所有代码均来自开源项目 personal-blog-backend,基于 Spring Boot 3.5 + Java 21 构建,欢迎 ⭐ Star。
一、项目整体模块结构
personal-blog-backend/ ← 根模块(packaging: pom)
│
├── blog-common/ ← 公共组件(统一响应、异常、工具类)
├── blog-modules/ ← 领域业务模块聚合
│ ├── blog-module-system/ ← 认证与用户管理
│ │ ├── blog-system-api/ ← 接口层(DTO/VO/Service 接口,无 Spring 依赖)
│ │ └── blog-system-service/← 实现层(Controller/ServiceImpl/Mapper/Entity)
│ ├── blog-module-article/
│ ├── blog-module-comment/
│ ├── blog-module-file/
│ └── blog-module-ai/
├── blog-application/ ← 启动入口(聚合所有模块,端口 8080)
└── blog-admin-server/ ← 独立监控中心(端口 9000)
关键设计原则:每个业务模块拆分为 -api 和 -service 两层:
-api模块:只包含 DTO、VO、Service 接口(interface)、Enum。零 Spring 依赖,可以被任意模块引用而不引发循环。-service模块:包含Controller、ServiceImpl、Mapper、Entity等实现。只对外暴露-api接口,其他模块不得直接依赖-service的实现类。
二、根 POM 统一版本管理
2.1 BOM(Bill of Materials)依赖管理
项目采用根 pom.xml 作为唯一版本来源(Single Source of Truth),所有子模块不单独声明版本号。
<!-- personal-blog-backend/pom.xml -->
<properties>
<java.version>21</java.version>
<spring-boot.version>3.5.12</spring-boot.version>
<mybatis-plus.version>3.5.14</mybatis-plus.version>
<jjwt.version>0.13.0</jjwt.version>
<springdoc.version>2.8.16</springdoc.version>
<caffeine.version>3.2.2</caffeine.version>
<langchain4j.version>1.12.2</langchain4j.version>
<archunit.version>1.4.1</archunit.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Boot BOM:自动管理 Spring 全家桶版本 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- MyBatis-Plus BOM -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-bom</artifactId>
<version>${mybatis-plus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
为什么用 BOM 而不是直接声明版本? Spring Boot 的依赖体系极其复杂,手动对齐版本极易出现兼容性冲突(
NoSuchMethodError、ClassCastException)。使用 BOM 后,同一框架的所有组件自动保持版本一致。
三、-api / -service 分层边界原则
以 blog-module-system 为例,其内部依赖关系如下:
blog-system-api ← 无 Spring 依赖,纯 Java
└── xxxService (interface)
└── xxxDTO / xxxVO
blog-system-service ← 引用 blog-system-api
└── xxxServiceImpl ── 实现 xxxService
└── xxxController ── 注入 xxxService(接口,非实现)
└── xxxMapper ── MyBatis-Plus
└── xxxEntity
其他模块需要使用用户信息时,只能引用 blog-system-api,不能直接 import blog-system-service 的实现类。这条规则由 ArchUnit 强制守护。
四、ArchUnit 架构守护
4.1 是什么,为什么用
ArchUnit 是一个基于 JUnit 的 Java 架构测试框架。原理:在测试时扫描 classpath 下的所有 .class 文件,构建依赖图,然后用定义的规则对图进行验证。
4.2 依赖引入
<!-- blog-application/pom.xml -->
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<scope>test</scope>
</dependency>
4.3 配置类:集中管理包路径
// blog-application/src/test/java/com/blog/architecture/config/ArchUnitConfig.java
public final class ArchUnitConfig {
public static final String CONTROLLER_PKG = "com.blog..controller..";
public static final String SERVICE_PKG = "com.blog..service..";
public static final String SERVICE_IMPL_PKG = "com.blog..service.impl..";
public static final String REPOSITORY_PKG = "com.blog..repository..";
public static final String ENTITY_PKG = "com.blog..entity..";
public static final String DTO_PKG = "com.blog..dto..";
public static final Map<String, String> MODULE_PACKAGES = ImmutableMap.of(
"system", "com.blog.system",
"article", "com.blog.article",
"comment", "com.blog.comment",
"file", "com.blog.file"
);
public static final Set<String> BUSINESS_MODULE_NAMES = MODULE_PACKAGES.keySet();
private ArchUnitConfig() {}
}
4.4 分层规则:强制单向依赖
// blog-application/src/test/java/com/blog/architecture/rules/LayerRule.java
public static final ArchRule LAYERED_ARCHITECTURE = layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy(CONTROLLER_PKG)
.layer("Service").definedBy(SERVICE_PKG)
.layer("Repository").definedBy(REPOSITORY_PKG)
.layer("Entity").definedBy(ENTITY_PKG)
.layer("DTO").definedBy(DTO_PKG)
.whereLayer("Controller").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller", "Service")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service")
.whereLayer("Entity").mayOnlyBeAccessedByLayers("Repository", "Service")
.whereLayer("DTO").mayOnlyBeAccessedByLayers("Controller", "Service", "Repository", "DTO");
4.5 模块隔离规则
// 禁止模块间循环依赖
public static final SliceRule NO_CYCLE_BETWEEN_MODULES = slices()
.matching("com.blog.(*)..").should().beFreeOfCycles()
.because("模块间循环依赖会导致微服务拆分失败");
// 禁止跨模块调用实现层(只能调用 -api 接口)
public static void checkNoCrossModuleImplDependency() {
ArchitectureTest.BUSINESS_MODULES.forEach(current -> {
String currentService = String.format("com.blog.%s.service..", current);
ArchitectureTest.BUSINESS_MODULES.stream()
.filter(other -> !other.equals(current))
.forEach(other -> {
String otherImpl = String.format("com.blog.%s.service.impl..", other);
noClasses().that().resideInAPackage(currentService)
.should().dependOnClassesThat().resideInAPackage(otherImpl)
.because(String.format("%s 模块不应依赖 %s 的实现层", current, other))
.check(ArchitectureTest.CLASSES);
});
});
}
五、踩坑记录
5.1 consideringAllDependencies() vs 默认模式
加了后会检查传递依赖,规则更严格但可能误报。建议先用默认模式,等稳定后再启用。
5.2 Lombok 生成的类被误判
在 ArchitectureTest 中过滤生成类:
ClassFileImporter importer = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.withImportOption(location -> !location.contains("$Builder"));
5.3 多模块 classpath 扫描路径
ArchUnit 测试必须写在 blog-application 的 test 目录下,否则扫描不到其他模块的类。
六、效果演示
违规时 mvn test 输出:
Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package
'com.blog.article.service..' should depend on classes that reside in a package
'com.blog.comment.service.impl..' was violated (1 times)
CI 失败,架构腐化被拦截在源头。
总结
| 实践 | 价值 |
|---|---|
| 根 POM 统一版本管理 | 消除版本冲突,单一升级入口 |
-api / -service 分层 | 模块边界物理可见,便于未来微服务拆分 |
| BOM 依赖管理 | Spring 全家桶版本一致性保障 |
| ArchUnit 架构守护 | 架构规则即测试,CI 自动拦截违规 |
参考资料
📌 专栏:Spring Boot 3 整合实战:主流技术栈深度整合 🔗 源码:github.com/liusxml/per…