
本文将详细介绍如何使用archunit定义并强制执行一项架构规则:确保每个存储库(repository)类只能被一个服务(service)类所依赖。我们将探讨如何通过自定义archcondition来精确检查依赖数量,并生成清晰的违规消息,从而有效维护应用模块间的单一职责和解耦性。
理解架构约束:存储库的单一服务依赖
在许多分层架构中,服务层(Service Layer)和数据访问层(Repository Layer)是常见的组件。通常,一个服务可以依赖多个存储库来完成其业务逻辑,但为了保持架构的清晰性、可维护性和单一职责原则,有时需要强制规定一个存储库只能被一个特定的服务所使用,即不允许存储库在多个服务之间共享。这种约束有助于避免复杂的交叉依赖和潜在的副作用。
ArchUnit是一个强大的Java架构测试库,它允许开发者以代码的形式定义和验证架构规则。接下来,我们将探讨如何使用ArchUnit来实现“存储库只能被一个服务使用”这一特定的架构规则。
初步规则:确保存储库仅被服务层使用
在强制单一服务依赖之前,一个更基础的规则是确保存储库类仅被服务层中的类所依赖,而不是被其他层(如控制器层或工具类)直接依赖。这可以通过以下ArchUnit规则实现:
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
public class RepositoryServiceRules {
// 假设这些是定义包名的常量
private static final String SUBPACKAGE_NAME_REPOSITORY = "..repository..";
private static final String SUBPACKAGE_NAME_SERVICE = "..service..";
@ArchTest
static final ArchRule repository_must_only_be_used_by_a_service =
classes().that().resideInAnyPackage(SUBPACKAGE_NAME_REPOSITORY)
.should().onlyHaveDependentClassesThat()
.resideInAnyPackage(SUBPACKAGE_NAME_SERVICE);
}这条规则定义了:所有位于 ..repository.. 包中的类,它们的所有依赖者(即使用它们的类)都必须位于 ..service.. 包中。这确保了存储库不会被服务层以外的组件直接访问。然而,这条规则并没有限制一个存储库可以被多少个服务使用,它仅仅确保了依赖者是服务。
强制单一服务依赖:使用自定义ArchCondition
为了实现“一个存储库只能被一个服务使用”的严格约束,我们需要检查每个存储库类的直接依赖者数量。ArchUnit提供了 ArchCondition 机制,允许我们定义复杂的自定义检查逻辑。
方案一:简洁的Lambda表达式与`describe`
ArchUnit允许使用 describe 方法结合Lambda表达式快速定义一个简单的 ArchCondition。这种方式适用于逻辑相对简单,且默认违规消息可以接受的情况。
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.Dependency;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.base.DescribedPredicate.describe;
import static com.tngtech.archunit.lang.conditions.ArchConditions.have;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
public class RepositoryServiceRules {
private static final String SUBPACKAGE_NAME_REPOSITORY = "..repository..";
private static final String SUBPACKAGE_NAME_SERVICE = "..service..";
@ArchTest
ArchRule repository_must_have_exactly_one_dependent_class =
classes().that().resideInAnyPackage(SUBPACKAGE_NAME_REPOSITORY)
.should(have(describe("#{dependent classes} == 1", javaClass ->
javaClass.getDirectDependenciesToSelf().stream()
.map(Dependency::getOriginClass).count() == 1
)));
}在这段代码中:
- classes().that().resideInAnyPackage(SUBPACKAGE_NAME_REPOSITORY) 选择了所有存储库类作为规则检查的对象。
- .should(have(describe(...))) 应用了一个自定义条件。
- describe("#{dependent classes} == 1", ...) 定义了一个描述性谓词。其中的字符串是当规则通过时显示的描述,而Lambda表达式 javaClass -> ... 则是实际的检查逻辑。
- javaClass.getDirectDependenciesToSelf() 获取了所有直接依赖于当前 javaClass 的依赖关系。
- .stream().map(Dependency::getOriginClass) 将这些依赖关系映射到它们的源类(即依赖者类)。
- .count() == 1 检查依赖者的数量是否恰好为1。
这个方案简洁有效,但当规则被违反时,生成的错误消息可能不够详细,仅显示“dependent classes == 1”的条件未满足。
方案二:自定义ArchCondition以生成更友好的违规消息
为了提供更具体、更易于理解的违规消息,我们可以实现一个完整的 ArchCondition。这在大型项目或复杂规则中尤其有用,因为它能帮助开发者快速定位问题。
import com.tngtech.archunit.core.domain.Dependency;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ConditionEvents;
import java.util.Set;
import java.util.stream.Collectors;
import static com.tngtech.archunit.lang.ConditionEvent.createMessage;
import static com.tngtech.archunit.lang.SimpleConditionEvent.violated;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toSet;
public class RepositoryServiceRules {
private static final String SUBPACKAGE_NAME_REPOSITORY = "..repository..";
private static final String SUBPACKAGE_NAME_SERVICE = "..service..";
@ArchTest
ArchRule repository_must_have_exactly_one_dependent_class_with_custom_message =
classes().that().resideInAnyPackage(SUBPACKAGE_NAME_REPOSITORY)
.should(new ArchCondition("have one dependent class") {
@Override
public void check(JavaClass javaClass, ConditionEvents events) {
// 获取所有直接依赖于当前存储库类的类
Set dependentClasses =
javaClass.getDirectDependenciesToSelf().stream()
.map(Dependency::getOriginClass)
.collect(toSet());
// 检查依赖者数量是否不为1
if (dependentClasses.size() != 1) {
String message;
if (dependentClasses.isEmpty()) {
// 如果没有依赖者
message = "has no dependent classes";
} else {
// 如果有多个依赖者,列出它们
message = dependentClasses.stream()
.map(JavaClass::getName)
.collect(joining(", ", "has several dependent classes: ", ""));
}
// 报告违规,并附带详细消息
events.add(violated(javaClass, createMessage(javaClass, message)));
}
}
});
} 这个自定义 ArchCondition 的实现提供了以下优点:
-
清晰的描述: new ArchCondition
("have one dependent class") 定义了规则的意图。 - check 方法: 这是核心逻辑所在,它接收 JavaClass(当前正在检查的类)和 ConditionEvents(用于报告违规)。
- 获取依赖者: 同样使用 javaClass.getDirectDependenciesToSelf().stream().map(Dependency::getOriginClass).collect(toSet()) 来获取所有依赖于当前存储库类的集合。
-
详细的违规消息:
- 如果 dependentClasses 为空,则报告“has no dependent classes”。
- 如果 dependentClasses 大于1,则列出所有依赖者的全限定名,例如“has several dependent classes: com.example.service.UserService, com.example.service.ProductService”。
- violated 和 createMessage: 这些方法用于向 ConditionEvents 报告具体的违规事件和消息。
通过这种方式,当ArchUnit测试失败时,开发者可以立即看到是哪个存储库类违反了规则,以及具体被哪些服务类共享,从而大大提高了问题排查的效率。
注意事项与最佳实践
- 包名约定: 示例中使用了 SUBPACKAGE_NAME_REPOSITORY 和 SUBPACKAGE_NAME_SERVICE 等常量来定义包路径。在实际项目中,应确保这些包名能够准确地匹配您的项目结构,并且具有良好的命名约定。
- getDirectDependenciesToSelf() 的理解: 这个方法返回的是所有直接依赖于当前 JavaClass 的 Dependency 对象。每个 Dependency 对象包含了依赖的源(getOriginClass())和目标(getTargetClass()),以及依赖的类型。
- 测试覆盖: 除了编写这些ArchUnit规则,还应确保这些规则被集成到CI/CD流程中,以便在代码提交时自动验证架构合规性。
- 规则粒度: 根据项目需求,您可以调整规则的粒度。例如,如果某些存储库确实需要被多个服务共享(例如,通用的用户认证存储库),则可能需要为这些特殊情况定义豁免规则,或者将它们放置在不同的包中,以便不被此规则约束。
- 可读性: 编写清晰的ArchUnit规则描述和详细的违规消息对于团队协作和长期维护至关重要。
总结
通过ArchUnit的自定义 ArchCondition 机制,我们可以灵活且精确地定义复杂的架构规则,例如强制存储库只能被单一服务依赖。这不仅有助于在开发早期发现架构问题,还能在项目演进过程中持续维护架构的健康性。无论是使用简洁的 describe 方法,还是实现更详细的 ArchCondition 来生成丰富的违规消息,ArchUnit都为Java项目的架构治理提供了强大的工具。










