在分层架构的项目中,仓库层通常作为数据访问的底层模块,服务层负责业务逻辑处理,合理的依赖关系应该是服务层仅依赖对应模块的仓库层,避免出现服务层跨模块依赖仓库、或者仓库层反向依赖服务层的情况。如果依赖关系混乱,会导致模块耦合度升高,后续代码迭代和维护的难度大幅增加。

什么是ArchUnit
ArchUnit是一款基于Java的架构约束测试框架,它可以在单元测试阶段扫描项目的字节码,根据预设的规则校验代码的包结构、类依赖、注解使用等是否符合架构要求,一旦出现违反规则的情况,测试会直接失败并给出明确的错误信息。
分层依赖的常见问题
在没有架构约束的情况下,项目中很容易出现以下不符合预期的依赖情况:
- 订单服务层的类直接依赖用户仓库层的类,跨模块调用仓库
- 仓库层的类引入服务层的接口或类,出现反向依赖
- 服务层同时依赖多个不同模块的仓库,职责边界模糊
用ArchUnit实现单一依赖校验
1. 引入依赖
首先在项目的pom.xml中引入ArchUnit的依赖:
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.1.0</version>
<scope>test</scope>
</dependency>
2. 定义分层规则
假设项目的包结构如下:
- com.example.service:服务层包,下分order、user等子包
- com.example.repository:仓库层包,下分order、user等子包
我们需要校验的规则是:服务层的类只能依赖同模块的仓库层类,不能依赖其他模块的仓库,也不能被仓库层依赖。
首先定义各个层的标识:
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.library.Architectures;
// 导入所有项目的类
JavaClasses importedClasses = new ClassFileImporter().importPackages("com.example");
// 定义分层架构
Architectures.LayeredArchitecture layeredArchitecture = Architectures.layeredArchitecture()
.layer("Service").definedBy("com.example.service..")
.layer("Repository").definedBy("com.example.repository..");
3. 编写单一依赖规则
接下来添加依赖约束,强制服务层只能访问仓库层,且只能访问同模块的仓库:
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
// 规则1:服务层只能依赖仓库层,不能依赖其他层
ArchRule serviceOnlyDependRepository = classes()
.that().resideInAPackage("com.example.service..")
.should().onlyDependOnClassesThat().resideInAnyPackage(
"com.example.service..", // 允许依赖同层
"com.example.repository..", // 允许依赖仓库层
"java..", // 允许依赖JDK类
"javax..",
"org.springframework.." // 允许依赖框架类
);
// 规则2:仓库层不能依赖服务层,避免反向依赖
ArchRule repositoryNotDependService = classes()
.that().resideInAPackage("com.example.repository..")
.should().notDependOnClassesThat().resideInAPackage("com.example.service..");
// 规则3:强制服务层子包只能依赖同子包的仓库层
// 例如com.example.service.order包的类只能依赖com.example.repository.order包的类
ArchRule orderServiceOnlyDependOrderRepo = classes()
.that().resideInAPackage("com.example.service.order..")
.should().onlyDependOnClassesThat().resideInAnyPackage(
"com.example.service.order..",
"com.example.repository.order..",
"java..",
"javax..",
"org.springframework.."
);
ArchRule userServiceOnlyDependUserRepo = classes()
.that().resideInAPackage("com.example.service.user..")
.should().onlyDependOnClassesThat().resideInAnyPackage(
"com.example.service.user..",
"com.example.repository.user..",
"java..",
"javax..",
"org.springframework.."
);
4. 执行测试
将规则放到JUnit5的测试方法中执行:
import org.junit.jupiter.api.Test;
public class ArchitectureTest {
@Test
void testServiceRepositoryDependency() {
JavaClasses importedClasses = new ClassFileImporter().importPackages("com.example");
// 执行所有规则
serviceOnlyDependRepository.check(importedClasses);
repositoryNotDependService.check(importedClasses);
orderServiceOnlyDependOrderRepo.check(importedClasses);
userServiceOnlyDependUserRepo.check(importedClasses);
}
}
规则校验效果
如果代码中出现了违反规则的情况,比如订单服务类依赖了用户仓库:
package com.example.service.order;
import com.example.repository.user.UserRepository; // 跨模块依赖,违反规则
public class OrderService {
private UserRepository userRepository; // 非法依赖
}
执行测试时会直接失败,错误信息会明确指出哪个类违反了哪条规则,方便开发人员快速定位问题:
ArchUnit error: Rule 'classes that reside in a package com.example.service.order.. should only depend on classes that reside in any package [com.example.service.order.., com.example.repository.order.., java.., javax.., org.springframework..]' was violated (1 times): Class <com.example.service.order.OrderService> depends on <com.example.repository.user.UserRepository> in a field of type <com.example.repository.user.UserRepository>
总结
通过ArchUnit编写架构约束测试,可以在开发阶段就拦截不符合分层依赖规则的代码,强制仓库层和服务层保持单一依赖关系,避免架构随着项目迭代逐渐腐化。这种测试可以集成到CI流程中,每次提交代码都自动执行校验,保证整个团队的代码架构始终符合预期。