首页 > Java > java教程 > 正文

使用ArchUnit强制执行单一依赖:服务与存储库的架构约束

聖光之護
发布: 2025-11-09 14:38:00
原创
731人浏览过

使用ArchUnit强制执行单一依赖:服务与存储库的架构约束

本文将详细介绍如何使用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
            )));
}
登录后复制

在这段代码中:

AppMall应用商店
AppMall应用商店

AI应用商店,提供即时交付、按需付费的人工智能应用服务

AppMall应用商店 56
查看详情 AppMall应用商店
  • 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<JavaClass>("have one dependent class") {
                @Override
                public void check(JavaClass javaClass, ConditionEvents events) {
                    // 获取所有直接依赖于当前存储库类的类
                    Set<JavaClass> 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<JavaClass>("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测试失败时,开发者可以立即看到是哪个存储库类违反了规则,以及具体被哪些服务类共享,从而大大提高了问题排查的效率。

注意事项与最佳实践

  1. 包名约定: 示例中使用了 SUBPACKAGE_NAME_REPOSITORY 和 SUBPACKAGE_NAME_SERVICE 等常量来定义包路径。在实际项目中,应确保这些包名能够准确地匹配您的项目结构,并且具有良好的命名约定。
  2. getDirectDependenciesToSelf() 的理解: 这个方法返回的是所有直接依赖于当前 JavaClass 的 Dependency 对象。每个 Dependency 对象包含了依赖的源(getOriginClass())和目标(getTargetClass()),以及依赖的类型。
  3. 测试覆盖: 除了编写这些ArchUnit规则,还应确保这些规则被集成到CI/CD流程中,以便在代码提交时自动验证架构合规性。
  4. 规则粒度: 根据项目需求,您可以调整规则的粒度。例如,如果某些存储库确实需要被多个服务共享(例如,通用的用户认证存储库),则可能需要为这些特殊情况定义豁免规则,或者将它们放置在不同的包中,以便不被此规则约束。
  5. 可读性: 编写清晰的ArchUnit规则描述和详细的违规消息对于团队协作和长期维护至关重要。

总结

通过ArchUnit的自定义 ArchCondition 机制,我们可以灵活且精确地定义复杂的架构规则,例如强制存储库只能被单一服务依赖。这不仅有助于在开发早期发现架构问题,还能在项目演进过程中持续维护架构的健康性。无论是使用简洁的 describe 方法,还是实现更详细的 ArchCondition 来生成丰富的违规消息,ArchUnit都为Java项目的架构治理提供了强大的工具。

以上就是使用ArchUnit强制执行单一依赖:服务与存储库的架构约束的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号