axon4.2

介绍

Axon 4.0 作为 Axon Framework 3.* 的后继版本发布。 名称从“Axon Framework”改为“Axon”是有意为之:从4.0开始,Axon不仅仅是一个框架,而是一个由Axon Framework和Axon Server组成的完整平台。 虽然可以使用 Axon 构建多种类型的应用程序,但事实证明它在微服务架构中非常受欢迎。 最近对 Axon 的许多更改都考虑到了这个用例。 请访问 AxonIQ 网站以了解有关 AxonIQ 和 Axon 社区的更多信息。 在那里,您将找到有关 Axon 培训、支持选项、即将发生和过去的事件的信息。

如何使用本指南

通过本指南,我们旨在提供您使用 Axon 特定代码创建和定制软件所需的所有信息,以满足您组织的需求。 请注意,从 GitBook 2.0 版开始,不再可能通过 gitbook-cli 在本地构建此参考指南。 因此,它只能由 GitBook 自己构建。 有关这方面的更多细节,建议阅读重要更改 CLI 工具链。

执照

Axon 平台由 Axon Framework 和 Axon Server 组成。 它由 AxonIQ B.V. 根据下述许可提供。 Axon 框架以及 Axon 文档和示例程序都是开源的,并在 Apache License v2.0 下提供。

对于 Axon Server,许可取决于版本。 标准版本称为“Axon Server”,是开源的,可以免费下载和使用。 它是根据 AxonIQ 特定的开源许可证提供的。 虽然此许可证允许您在任何环境中自由运行软件,但它不如 Apache 许可证宽松。 例如,它不允许您作为被许可人创作衍生作品。 称为“Axon Server Enterprise”的商业版本以及各种“Packs”是封闭源代码并根据商业许可协议提供。 如果您对许可有任何疑问,或想获得我们商业软件的许可,请通过 axoniq.io 或 sales@axoniq.io 与我们联系。

快速开始

此页面向您展示如何使用 Axon 平台开始构建您自己的应用程序。 通过下载 QuickStart 存档获得您需要的一切。 它包含一个简单的演示应用程序,旨在展示平台的各个方面。 解压 AxonQuickStart-VERSION.zip 运行 Axon 服务器: $ java -jar AxonServer/axonserver-VERSION.jar Axon Server Web 仪表板应该可以在此处获得 http://localhost:8024/ 运行演示应用程序: $ cd giftcard-demo && ./mvnw spring-boot:run 演示应用程序应该在这里 http://localhost:8080/ 探索 README.md Axon 编码教程 #1:- Axon 应用程序的结构

架构概览

基于 Axon 的应用程序遵循一种架构模式,该模式基于领域驱动设计 (DDD)、命令查询责任分离 (CQRS) 和事件驱动架构 (EDA) 的原则。 这些原则的结合使基于 Axon 的应用程序更加健壮和适应性强,以适应我们业务领域变化所需的变化。

Axon 可用于大型单体应用程序,其中内部结构对于保持单体应用程序的适应性至关重要,以及微服务,其中系统的分布式特性增加了复杂性。

以下部分描述了不同原则之间的关系以及 Axon 如何使用这些原则来帮助您构建更易于维护、性能更高和更可靠的软件。

处理复杂性

Axon 起源于试图为不断增加的意外复杂性找到解决方案。 应用领域驱动设计中的概念将在很大程度上有所帮助,即使是设计最好的模型也不会在生产中自行运行。

虽然 Axon 对与域模型的交互应该如何进行有自己的看法,但它试图避免对建模自由的任何限制。 即使您的意见与 Axon 的意见不同,也有足够的钩子、配置选项和触发器来改变 Axon 行为的某些方面。 您将在整个参考指南中找到这些内容。

DDD & CQRS

领域驱动设计 (DDD) 描述了一种构建软件的方法,该方法非常重视模型的设计,利用无处不在的语言。 领域模型是软件的核心,应该正确捕捉和处理领域的本质复杂性。

命令查询职责分离 (CQRS) 是一种架构模式,它描述了处理命令(更改应用程序状态的请求)和回答查询(请求有关应用程序状态的信息)的应用程序部分之间的区别。

当结合 DDD 和 CQRS 时,可以将应用程序划分为多个组件,其中每个组件要么提供有关应用程序状态的信息,要么更改应用程序的状态。 这些组件中的每一个都有一个专注于这些职责的模型。

下图显示了基于 Axon 的应用程序的典型架构。

 

基于 CQRS 的 Axon 应用程序的架构概述

在这样的架构中,UI(或 API)可以发送命令来请求更改应用程序的状态。 这些命令由命令处理组件处理,该组件使用模型来验证命令并决定触发哪些副作用(如果有)。

 

命令引起的副作用使用事件发布。 这些事件由一个或多个采取适当行动的事件处理组件获取。 一个典型的动作是更新视图模型,它允许 UI 呈现应用程序的状态。 其他操作可能是向外部组件发送消息,甚至通过新命令触发其他副作用。

命令模型和查询模型(也称为视图模型或投影)的分离允许这些模型只关注应用程序的特定方面。 这使得每个单独的模型更容易理解,因此从长远来看更易于维护。

 

业务逻辑与基础设施分离

 

 意外复杂性的增加通常是由泄漏的抽象引起的,其中基础设施问题与业务逻辑混合在一起。 Axon 将两者严格分开作为首要任务。 在 Axon 设计的任何地方,它都明确区分了您想要做什么(例如发布事件)和实际完成方式(例如事件发布实现)。

这使得 Axon 具有极强的可配置性,并且能够适应您的特定情况。 更重要的是,它将意外的复杂性降至最低。 例如,虽然 Axon 可以轻松实现事件源聚合,但它绝不强制聚合为事件源。 Repository 接口完全抽象了这个决定。 此外,决定通过命令总线发送命令的组件决不负责决定该消息如何传输到处理程序。

Axon 不仅通过为组件提供清晰的接口来实现这种分离,它还结合了配置 API 中的基础设施选择,在那里业务逻辑组件与应用程序的基础设施方面分开配置。

 

显式消息传递

Axon 强烈利用显式消息对象的使用。 这意味着基于 Axon 的应用程序中的每个消息通常由该应用程序中的特定 Java 类表示。 虽然这在编写基于 Axon 的应用程序时确实会产生一些开销,但它确实带来了一些优势:

使用显式消息可以更容易地将它们透明地分发到远程组件;

显式消息的使用强调消息设计,这已被证明对应用程序的长期可维护性很重要;

可以轻松存储显式消息以供以后处理

虽然消息是 Axon 的核心概念,但并非所有消息都是平等的。 不同的意图需要不同的路由模式。 例如,对于某些消息,人们会期待结果,而其他消息则本质上是即发即忘。

Axon 将 Messages 大致分为三类:

命令; 表达改变应用程序状态的意图。 命令被路由到单个目的地并可能提供响应。

查询; 表达对信息的渴望。 根据调度策略,查询可以同时路由到一个或多个目的地。

事件; 表示发生了相关事件的通知。 事件发布到任何感兴趣的组件,并且不提供任何形式的返回值。

位置透明度

使用显式消息的最大好处是,相互交互的组件不需要知道它们对应的位置。 事实上,在大多数情况下,发送组件甚至对消息的实际目的地都不感兴趣。 我们称之为“位置透明度”。

与将服务置于逻辑 URL 之后相比,Axon 进一步提高了位置透明度。 在 Axon 中,发送消息的组件不需要为该消息指定目的地。 消息根据它们的构造型(命令、查询或事件)和它们携带的有效负载类型进行路由。 Axon 使用应用程序的功能自动为消息找到合适的目的地。

由位置透明组件构成的系统使该系统具有高度的适应性。 例如,由单独使用命令、事件和查询进行通信的分离良好的组件构建的整体系统可以轻松拆分为单独部署的单元,而不会对功能产生任何影响。

 

通过位置透明的微服务演进

这使得 Axon 非常适合微服务环境。 逻辑可以轻松地在已部署组件之间移动,而不会影响整个系统的功能方面。 然后可以主要根据该系统的每个单独组件的非功能性需求来决定逻辑的位置。 例如,具有明显不同性能特征的组件或需要不同发布周期的组件,可以从整体应用程序中分离出来,以减少对该组件更改的影响。

 事件溯源

在许多系统中,事件得到了很多额外的关注。 虽然 Axon 清楚地承认并非每条消息都是一个事件(还有命令和查询),但事件有一些特别之处。

事件保留价值。 如果命令和查询的值在触发其副作用或提供其结果时显着降低,则事件表示已发生的事情,在事件发生后很长时间内了解这可能很有用。

事件为审计跟踪提供了非常好的粒度级别。 但是,要使审计跟踪 100% 可靠,它不仅应该作为副作用生成,还必须能够确保审计跟踪正确反映任何决策。

事件溯源是一个过程,其中事件不仅作为命令的副作用生成,而且还形成状态的来源。 虽然应用程序的当前状态并未显式存储在数据库中,但它被隐式存储为一系列可用于导出当前状态的事件。 收到命令后,应用程序的状态将从存储在数据库中的事件动态派生,然后决定应用哪些副作用。

自行实施事件溯源可能非常复杂。 Axon 提供了必要的 API,使构建命令模型变得非常简单,甚至是一种更自然的方法。 Axon 的测试fixture有助于确保正确遵循某些准则和要求。

拥有可靠的审计跟踪不仅被证明对系统的可审计性很有用,它还提供了构建新视图模型、进行数据分析和为机器学习算法提供坚实基础所需的信息。

DDD 和 CQRS 概念

 

 Axon 很大程度上基于领域驱动设计 (DDD) 和命令查询职责分离的原则。 虽然对这些概念的完整解释超出了本参考指南的范围和意图,但我们确实希望提供 Axon 应用程序上下文中最重要概念的摘要。

战略理念

战略概念是在体系结构层面影响系统设计的相对高级概念。 它提供了设计组件边界和它们之间交互的概念。 虽然它们不会直接影响单个 Axon 应用程序的设计,但此类应用程序的边界通常受这些概念的影响。

域和子域

DDD 上下文中域的正式定义是:

知识、影响或活动的领域。 用户应用程序的主题领域是软件的领域。

这个定义可能看起来很模糊,但它确实很好地抓住了本质。 域基本上是您构建软件的环境。 这种环境由法律、最佳实践、期望、传统等组成,它们定义了什么是重要的,什么是不重要的。

域可以非常大,域内的不同区域可能具有不同的影响。 例如,在银行领域,消费银行、企业银行和财富管理之间存在明显区别。

有许多技术可以帮助您发现域。 事件风暴是一个特别有趣的事件。 它是一种用于快速探索复杂业务领域的研讨会形式。

模型

一个模型是:

描述域的选定方面并可用于解决与该域相关的问题的抽象系统;

换句话说,模型捕获了对我们解决领域内特定问题重要和有帮助的内容。 这个定义本身表明一个应用程序应该由多个模型组成,因为每个问题都有不同的理想模型来解决它。

有关基于 Axon 的应用程序中模型的一些构建块的更多信息,请参阅战术概念。

有界上下文

上下文是:

单词或语句出现的环境决定了其含义。

换句话说,相同的领域概念对不同的人可能有不同的含义。

例如,考虑飞行的概念。 对于乘客而言,航班是指从飞机起飞到到达目的地之间的时间段。 然而,地勤人员关心航班到达登机口、登机的餐食、枕头等数量,并且在航班离开登机口时完成。 对他们来说,出发时间是截止日期,而不是起点。

因此,模型和上下文有许多规则:

明确定义模型适用的上下文。

在团队组织、应用程序特定部分的使用以及代码库和数据库模式等物理表现方面明确设置边界。

在这些范围内保持模型严格一致,但不要被它之外的问题分心或混淆。

上下文映射

有界上下文永远不会完全独立存在。 来自不同上下文的信息最终将被同步。 显式地对这种交互进行建模很有用。 领域驱动设计命名了上下文之间的一些关系,这些关系驱动了它们的交互方式:

伙伴关系(两个环境/团队共同努力建立互动)

客户-供应商(上游/下游关系的两个团队 - 上游可以独立于下游团队成功)

顺从(上下游关系的两个团队——上游没有动力提供给下游,下游团队不努力翻译)

共享内核(明确地,共享模型的一部分)

分开的方式(把它们剪松)

反腐败层(下游团队构建了一个层,通过转换交互来防止上游设计“泄漏”到他们自己的模型中)

在基于 Axon 的应用程序中,上下文定义了事件携带值的边界。 有些事件可能仅在它们发表的上下文中有价值,而其他事件甚至可能在外部也有价值。 事件(或任何消息,在这方面)发布的范围越广,最终耦合到发送者的组件就越多。

战术概念

为了构建模型,DDD(在某种程度上也是 CQRS)提供了许多有用的构建块。 以下是一些在基于 Axon 的应用程序上下文中很重要的构建块。

聚合体

聚合是始终保持一致状态(在单个 ACID 事务中)的一个实体或一组实体。 聚合根是聚合内负责维护这种一致状态的实体。 这使得聚合成为在任何基于 CQRS 的应用程序中实现命令模型的主要构建块。

DDD 的正式定义是:

一组相关联的对象,它们被视为一个单元以进行数据更改。 外部引用仅限于聚合的一个成员,指定为根。 一组一致性规则适用于聚合的边界内。

在基于 CQRS 的应用程序中,聚合非常明确地出现在命令模型中,因为这是启动更改的地方。 但是,查询模型/投影也是由聚合构成的。 然而,一般来说,查询模型中的聚合要简单得多,因为这些模型中的状态不变量通常不那么严格。

Saga

并非每个命令都能够在单个原子事务中完全执行。作为交易的论据经常出现的一个非常常见的例子是汇款。人们通常认为,将资金从一个帐户转移到另一个帐户绝对需要原子性且一致的交易。好吧,事实并非如此。相反,这是完全不可能做到的。如果资金从 A 银行的账户(BankAccount 的实例 A)转移到银行 B 的另一个账户(BankAccount 的实例 B)怎么办?银行 A 是否获得了银行 B 数据库中的锁?如果在转账中,A银行扣了款,B银行还没有存,是不是很奇怪?不是真的,它正在“进行中”。另一方面,如果在将钱存入银行 B 的帐户时出现问题,银行 A 的客户会要求取回他的钱。因此,我们确实希望最终实现某种形式的一致性。另一个例子可能是 GiftCardPaymentSaga,它会在下订单后启动 (OrderPlacedEvent)。它将确保一旦礼品卡成功兑换(CardRedeemedEvent),另一方确认订单(ConfirmGiftCardPaymentCommand)。

虽然在某些情况下 ACID 事务不是必需的,甚至是不可能的,但仍然需要某种形式的事务管理。 通常,这些事务被称为 BASE 事务:基本可用、软状态、最终一致性。 与 ACID 不同,BASE 事务不能轻易回滚。 要回滚,需要采取补偿措施来恢复作为事务一部分发生的任何事情。 在礼品卡示例中,礼品卡兑换失败,将拒绝订单付款。

在 CQRS 中,可以使用 Sagas 来管理这些 BASE 事务。 它们响应事件并可能调度命令、调用外部应用程序等。在领域驱动设计的上下文中,通常将 Sagas 用作不同聚合(或聚合实例)之间的协调机制,以最终实现一致性。

查看模型或投影

在 CQRS 中,视图模型(也称为投影或查询模型)用于有效地公开有关应用程序状态的信息。 与命令模型不同,视图模型关注数据,而不是行为。 视图模型通常被建模以适应特定受众的信息需求。 这些模型应该清楚地表达模型的目标受众,以防止“分心”和范围蔓延,这最终会导致可维护性甚至性能的损失。

事件驱动的微服务

DDD 和 CQRS 概念一章中描述的概念在设计和创建(事件驱动)微服务系统时非常适用。 在本章中,我们将明确列出在此类环境中应用 Axon 的一些常见策略。

进化微服务

在 AxonIQ,我们相信系统会演变成微服务,而不是尝试从头开始构建微服务系统。 主要原因是探索合理的上下文边界(参见有界上下文)和模型需要时间。 在分布式系统中改变这些边界比在单体系统中困难得多。

Axon 利用组件的分离并在它们之间使用显式消息传递,这使得这些组件位置透明。 与使用服务发现不同,Axon 用于消息传递的方法根本不需要组件知道消息的目的地。 它们会自动路由到宣传处理此类消息的能力的组件。 这使得这些系统比基于“常规”微服务的系统更灵活地更改。

应用 Axon 的策略

在微服务环境中应用 Axon 有不同的策略。 可以在系统级别采用 Axon 理念并使用 Axon 构建所有服务。 但是,Axon 在仅将其应用于单个应用程序/服务时也已经很有用。 最后,我们还将讨论在多语言环境中使用 Axon 时的具体策略。 为此,Axon 在构建时考虑了集成。

基于 Axon 的微服务

在系统级别使用 Axon 时,意味着多个服务运行 Axon(或兼容的 API),可以最大程度地使用消息传递概念。 应用程序可以简单地使用不同的消息总线来发送和接收来自其他组件的消息。 这使得系统在更改组件的部署策略时非常灵活。

仅在单个服务中使用 Axon

在现有微服务系统中构建单个基于 Axon 的服务时,您可能希望使用“传统”其余端点公开您的 API。 在这种情况下,基于 Axon 的应用程序将需要一个小的 API 层,将 REST 调用转换为命令,然后在内部将这些命令分派到命令总线。 但是,请注意,请求可能不会一致地路由,然后同一聚合的命令可能会路由到不同的实例。

如果基于 Axon 的服务部署了多个实例,您仍然可以从使用总线的分布式实现中受益,以允许这些实例正确平衡它们之间的消息处理。

混合/多语言环境

在实践中,许多基于微服务的系统在多语言环境中运行。 不同的服务将运行在不同的技术栈上。 在这些环境中,确保上下文边界得到适当保护并在适用的情况下提供适当的反腐败层更为重要。

不太可能所有使用的技术堆栈都遵循与 Axon 应用程序相同的基于消息传递的方法。 然而,这并不意味着需要放弃这些概念。 您仍然可以从显式消息传递的许多优点中受益。 在这样的环境中,反腐败层可以作为处理命令、事件和查询的组件来实现,并执行对外部服务的其他类型的调用(例如 REST 调用)。 这样,使用显式消息传递的组件就无需担心轮询外部服务以进行更改,也无需受到不同类型 API 带来的技术挑战的影响。

Axon 支持不同类型的连接器,这些连接器允许将事件(在某些情况下也包括其他消息类型)发布到第三方消息代理。 默认情况下,Axon 将对这些外部事件的格式做出假设,但它们始终可以被覆盖。 阅读有关这些特定扩展的章节以获取更多详细信息。

Axon Server

在消息驱动的微服务环境中,服务之间的通信高效、可靠且易于管理和监控非常重要。 消息路由不需要任何手动配置,并且添加新服务应该很容易。

使用 Axon 框架,应用程序开发人员隐藏了内部通信。 Axon Server 在分布式环境中提供了同样的体验。 它是一个易于使用、易于管理的平台,可处理所有事件、命令和查询。

Axon Server 了解正在交换的不同类型的消息:事件,从一个服务发送到一个或多个其他服务,通知服务发生了某些事情,命令,发送到一个服务做某事,可能等待 结果查询,发送到一个或多个服务以检索信息。

这些消息中的每一个都需要不同的策略,所有这些都由 Axon 服务器支持。

应用程序连接到消息传递平台并注册其功能。 一个应用程序可能能够执行一组特定的命令,另一个应用程序可能会处理多个查询。 同一个应用程序可能有多个实例连接到消息传递平台,每个实例都具有相同的能力。

消息模式

客户端或应用程序向消息传递平台发送请求。 平台找到合适的连接应用程序来发送请求,并转发请求。 它接收一个或多个回复,并将其转发给调用者。 命令总是发送到一个应用程序。 同一聚合的命令总是发送到同一个应用程序实例,以避免聚合的并发更新出现问题。 查询被发送到所有能够回答查询的应用程序。 如果同一应用程序有多个实例,则查询仅发送到其中一个实例。 事件存储在事件存储中并发送到所有注册的侦听器。

高可用

Axon Server 可以在集群模式下运行。 集群中的每个节点都是活动的,应用程序在这些节点上动态负载平衡。 同一应用程序的实例将连接到同一消息传递节点以优化性能。 不同应用程序的实例分布在节点上以分散负载。

流量控制

Axon 服务器控制发送到消息处理程序的消息流。 消息处理程序向消息平台发送了多个许可,指示消息平台可以发送的消息数量。 一旦处理程序准备好接受更多请求,它就会发送另一条带有许多额外许可的消息。 当处理程序没有许可时,Axon 服务器将消息排队。 当处理程序断开连接而仍有排队消息时,这些消息将重新路由到另一个处理程序(如果可能)。

 QoS 消息

客户端可以指示其请求的优先级。 这样,例如,来自批处理的命令可以以低于在线请求的优先级执行。

访问控制

需要授予应用程序访问消息传递平台的权限。 这避免了随机客户端开始向系统发送命令的风险。

实现技术

Axon Server 完全用 Java 开发,构建在 Spring Boot 上。 它作为单个 jar 文件分发,其中包含通过着色使用的所有库。

Axon Server 的配置信息存储在一个小型 h2 数据库中。 这包含有关消息传递平台节点和具有访问权限的应用程序的信息。 此信息会在集群中的节点之间自动复制。

消息传递平台有两种类型的接口:

HTTP

gRPC (HTTP 2.0)

标准 Axon 框架 Axon 服务器客户端和消息传递平台之间的通信使用 gRPC。

发行说明

此处记录了我们主要和次要版本中引入的所有增强功能和功能。 这包括对 Axon 框架、Axon 服务器和 Axon 框架扩展的改进,因为这三者都遵循相同的发布节奏。 对于每个项目的错误修复,我们参考此页面。

4.2 版

Axon 框架应用程序现在可以使用标签来支持 Axon 客户端和 Axon 服务器实例之间的“位置感知”级别。

 

此处进一步描述了此功能。

Axon Server 已经支持多个上下文,但 Axon Framework 应用程序无法指定应将哪个上下文消息分派到。

 

Axon 服务器连接器已通过 TargetContextResolver 进行扩展以允许这样做。

实现了 StreamablbeMessageSource 的新实现:MultiStreamableMessageSource。

此实现允许将多个“可流式传输”消息源配对为单个源。 这又可以用于例如从单个跟踪事件处理器的多个不同上下文中读取事件。

处理程序执行异常现在允许在分布式设置的情况下通过线路发送回应用程序特定信息。

TrackingToken 接口现在通过 position() 方法提供它在事件流中的相对位置的估计。

可选的返回类型现在可用于查询处理方法。

有关所有功能、增强功能和错误的完整列表,请查看问题跟踪器

4.1 版

TrackingEventProcessor 现在有一个 API,用于在应用程序运行时拆分和合并 TrackingToken。

Axon Server 添加到 UI 以拆分和合并给定的跟踪事件处理器的令牌。

除了 Dropwizard 指标,该框架现在还支持 Micrometer 指标。

MessageMonitor 接口用于允许与 Micrometer 集成。

最后,我们非常感谢这是作为社区贡献引入的。

现在支持原始类型作为 @QueryHandler 返回类型。

以与 CommandGateway 和 QueryGateway 类似的方式,我们引入了 EventGateway。

与命令和查询版本一样,EventGateway 提供了一个更简单的 API,用于在 EventBus 上分派事件

我们参考页面以获取所有更改的完整列表。

4.0 版

Axon Framework 的包结构发生了巨大变化,旨在为用户提供挑选和选择的选项。

例如,如果只需要框架的消息传递组件,则可以直接依赖 axon-messaging 包。

在包重组中,所有利用另一个框架提供额外内容的组件都被赋予了自己的存储库。

这些存储库称为 Axon 框架扩展。

事件处理器的配置已被替换并通过添加 EventProcessingConfigurer 进行了极大的微调。

版本 4.0 中引入了一些新的默认值,例如对期望与 Axon Server 连接的偏见。

另一个重要的机会是从默认切换到跟踪处理器而不是订阅处理器。

CommandResultMessage 的概念已被引入作为针对命令处理结果的专用消息。

为了简化配置并更轻松地克服弃用, Builder 模式已针对所有基础架构组件实现。

有关更多详细信息,请查看此处的问题列表。

 

配置

先决条件

系统上应安装 Java 8+ JRE。

Maven/Gradle 依赖

Axon 框架由许多提供特定功能的模块组成。 根据您项目的确切需求,您将需要包含一个或多个这些模块。

目前有两种获取模块二进制文件的方法:从我们的网站下载二进制文件,或者最好为您的构建系统(MavenGradle)配置一个存储库。

Axon 模块在 Maven Central 上可用。

主要模块

Axon '主要模块' 是经过彻底测试并且足够强大的模块,可以在苛刻的生产环境中使用。 所有这些模块的 maven groupId 是 org.axonframework。 访问 Maven Central Repository 以复制您需要的版本的坐标。

笔记

Axon Spring Boot Starter 模块是 Axon 项目的最快启动方式,因为它将以传递方式检索所有必需的模块/依赖项。 或者,您可以手动选择单个模块进行自定义配置。

Module

Artifact Id

Group Id

Maven Central

Axon Messaging

axon-messaging

org.axonframework

available

Axon Modeling

axon-modelling

org.axonframework

available

Axon Event Sourcing

axon-eventsourcing

org.axonframework

available

Axon Configuration

axon-configuration

org.axonframework

available

Axon Test

axon-test

org.axonframework

available

Axon Server Connector

axon-server-connector

org.axonframework

available

Axon Spring

axon-spring

org.axonframework

available

Axon Spring Boot Starter

axon-spring-boot-starter

org.axonframework

available

Axon Disruptor

axon-disruptor

org.axonframework

available

Axon Metrics

axon-metrics

org.axonframework

available

Axon Micrometer

axon-micrometer

org.axonframework

available

Axon Legacy

axon-legacy

org.axonframework

available

Axon Messaging
该模块包含所有必要的组件和构建块,以支持命令、事件和查询消息传递。
Axon Modeling
该模块包含创建域模型所需的组件,如聚合和 Sagas。
Axon Event Sourcing
该模块包含所有必要的基础设施组件,以支持事件溯源、命令和查询模型。
Axon Test
该模块包含可用于测试基于 Axon 的组件的测试装置,例如您的命令处理程序、聚合和 Sagas。 您通常在运行时不需要此模块,只需将其添加到类路径中即可运行测试。
Axon Configuration
该模块包含配置 Axon 应用程序所需的所有组件。
Axon Server Connector
该模块提供连接到 Axon 服务器的基础设施组件。
Axon Spring
该模块允许在 Spring 应用程序上下文中配置 Axon 框架组件。 它还提供了许多特定于 Spring Framework 的构建块实现,例如用于在 Spring 消息传递通道上发布和检索 Axon 事件的适配器。
Axon Spring Boot Starter
此模块为您的项目提供 Spring Boot 自动配置。 这是迄今为止最简单的入门选项,因为它会自动配置所有 Axon 组件。 这里有更详细的解释。
Axon Disruptor
该模块包含基于 Disruptor 范例的特定 CommandBus 和命令处理解决方案。
Axon Metrics
该模块提供了基于 Coda Hale 收集监控信息的基本实现。
Axon Micrometer
该模块提供了基于 Micrometer 收集监控信息的基本实现。 Micrometer 是一个维度优先的度量集合外观,其目的是让您可以使用供应商中立的 API 来计时、计数和衡量您的代码。
Axon Legacy
该模块包含支持旧 Axon 项目迁移以使用最新 Axon 版本的组件。
 
扩展模块
除了主要模块之外,还有几个扩展模块可以补充 Axon 框架。 它们解决了 Axon 框架对非 Axon 服务器解决方案的分发问题。 这些扩展的 maven groupId 以 org.axonframework.extensions.* 开头。 访问 Maven Central Repository 以复制您需要的版本的坐标。

Module

Artifact Id

Group Id

Maven Central

Axon AMQP

axon-amqp

org.axonframework.extensions.amqp

available

Axon AMQP Spring Boot Starter

axon-amqp-spring-boot-starter

org.axonframework.extensions.amqp

available

Axon Kafka

axon-kafka

org.axonframework.extensions.kafka

available

Axon Kafka Spring Boot Starter

axon-kafka-spring-boot-starter

org.axonframework.extensions.kafka

available

Axon Spring Cloud

axon-springcloud

org.axonframework.extensions.springcloud

available

Axon Spring Cloud Spring Boot Starter

axon-springcloud-spring-boot-starter

org.axonframework.extensions.springcloud

available

Axon JGroups

axon-jgroups

org.axonframework.extensions.jgroups

available

Axon JGroups Spring Boot Starter

axon-jgroups-spring-boot-starter

org.axonframework.extensions.jgroups

available

Axon Mongo

axon-mongo

org.axonframework.extensions.mongo

available

Axon CDI

axon-cdi

org.axonframework.extensions.cdi

available

Axon Tracing

axon-tracing

org.axonframework.extensions.tracing

available

Axon Tracing Spring Boot Starter

axon-tracing-spring-boot-starter

org.axonframework.extensions.tracing

available

Axon AMQP
该模块提供的组件允许您利用基于 AMQP 的消息代理作为事件消息分发机制。 这允许保证交付,即使在事件处理程序节点暂时不可用时也是如此。
Axon AMQP Spring Boot Starter
该模块在 axon-amqp 模块之上提供 Spring 自动配置。
Axon Kafka
该模块提供与 Kafka 的集成以进行事件分发。 因此,它扮演着与 Axon AMQP 扩展类似的角色,因此不是替代事件存储机制。 Kafka 是一个分布式消息流平台。
Axon Kafka Spring Boot Starter
该模块在 axon-kafka 模块之上提供 Spring 自动配置。
Axon Spring Cloud
该模块提供与 Spring Cloud 的集成以进行命令分发。 Spring Cloud 为常见的分布式系统模式提供了 API。
Axon Spring Cloud Spring Boot Starter
该模块在 axon-springcloud 模块之上提供 Spring 自动配置
Axon JGroups
该模块提供与 JGroups 的集成以进行命令分发。 JGroups 应该被视为一个可靠的消息传递工具包。
Axon JGroups Spring Boot Starter
该模块在 axon-jgroups 模块之上提供 Spring 自动配置
Axon Mongo
该模块提供事件和 saga 存储实现,用于在 MongoDB 数据库中存储事件流和 saga。 MongoDB 是一个基于文档的 NoSQL 数据库。
Axon CDI
该模块为 Java EE 平台提供对上下文和依赖注入 (CDI) 的支持。
Axon Tracing
该模块为 Axon 应用程序的分布式跟踪提供支持。 Open Tracing 标准用于提供跟踪功能。
Axon Tracing Spring Boot Starter
该模块在 axon-tracing 模块之上提供 Spring 自动配置
 

Spring Boot

Axon Framework 为 Spring 提供了广泛的支持,但不要求您使用 Spring 才能使用 Axon。 所有组件都可以通过编程方式进行配置,并且在类路径上不需要 Spring。 但是,如果您确实使用 Spring,则使用 Spring 的注解支持会使大部分配置变得更容易。 Axon 在此基础上提供 Spring Boot 启动器,因此您也可以从自动配置中受益。Auto-configuration

Auto-configuration

Axon 的 Spring Boot 自动配置是迄今为止开始配置 Axon 组件的最简单选项。 通过简单地声明对 axon-spring-boot-starter 的依赖,Axon 将自动配置基础设施组件(命令总线、事件总线、查询总线),以及运行和存储聚合和传奇所需的任何组件。

Demystifying Axon Spring Boot Starter

由于后台发生了很多事情,有时很难理解注释或仅包含依赖项如何启用这么多功能。

axon-spring-boot-starter 在构建 starter 时遵循一般的 Spring boot 约定。 它依赖于 axon-spring-boot-autoconfigure,它包含 Axon 自动配置的具体实现。 当 Axon Spring Boot 应用程序启动时,它会在类路径中查找名为 spring.factories 的文件。 该文件位于 axon-spring-boot-autoconfigure 模块的 META-INF 目录中:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    org.axonframework.springboot.autoconfig.MetricsAutoConfiguration,\
    org.axonframework.springboot.autoconfig.EventProcessingAutoConfiguration,\
    org.axonframework.springboot.autoconfig.AxonAutoConfiguration,\
    org.axonframework.springboot.autoconfig.JpaAutoConfiguration,\
    org.axonframework.springboot.autoconfig.JpaEventStoreAutoConfiguration,\
    org.axonframework.springboot.autoconfig.JdbcAutoConfiguration,\
    org.axonframework.springboot.autoconfig.TransactionAutoConfiguration,\
    org.axonframework.springboot.autoconfig.NoOpTransactionAutoConfiguration,\
    org.axonframework.springboot.autoconfig.InfraConfiguration,\
    org.axonframework.springboot.autoconfig.ObjectMapperAutoConfiguration,\
    org.axonframework.springboot.autoconfig.AxonServerAutoConfiguration

该文件映射了 Axon Spring 启动应用程序将尝试应用的不同配置类。 因此,根据此代码段,Spring Boot 将尝试应用 AxonServerAutoConfiguration、AxonAutoConfiguration、...的所有配置类。

这些配置类是否会被应用,这将取决于在这些类上定义的条件:

AxonServerAutoConfiguration 将 Axon 服务器配置为命令总线、查询总线和事件存储的实现。 它将在 AxonAutoConfiguration 之前应用,并且仅当 org.axonframework.axonserver.connector.AxonServerConfiguration 类在类路径中可用时才会应用。 可以通过在 .properties/.yml 文件中将 axon.axonserver.enabled 属性设置为 false 来禁用 Axon 服务器自动配置。

AxonAutoConfiguration 配置命令总线、查询总线、事件存储/事件总线和其他 Axon 组件的“non-axon-server”实现。 这些组件只有在它们不在 Spring Application 上下文中时才会被初始化,例如。 @ConditionalOnMissingBean(EventBus.class)。 由于 AxonAutoConfiguration 将在 AxonServerAutoConfiguration 之后应用,这些 Axon 组件将已经在 Spring 应用程序上下文中,因此 Axon Server 的命令总线、查询总线和事件存储/事件总线的实现将获胜。

Axon Spring Boot 自动配置不是侵入性的。 它将仅定义您尚未在应用程序上下文中明确定义的 Spring 组件。 这允许您通过在 @Configuration 类之一中定义自己的 bean 来完全覆盖自动配置的 bean。

本指南的以下部分将详细解释特定的 Axon (Spring) 组件配置。

Spring Boot 开发者工具通知

Spring Boot 开发人员工具是您项目的一个很好的补充,可以增强开发人员的体验。 但是,将 spring-boot-devtools 引入您的项目将强加一些类加载操作,这些操作已证明与 Axon 框架无法正常工作。 我们了解情况并正在寻找解决方案。

同时,我们建议在开发 Axon 应用程序时不要包含 spring-boot-devtools。

Starting the Axon Server

启动和运行 Axon Server 的方法不止一种:

在本地启动 Axon 服务器

Axon 下载页面包含服务器本身和 CLI 的可执行 JAR 文件。 将 axonserver.jar 复制到您选择的目录。 由于 Axon Server 使用合理的默认值,您现在可以开始使用了。 使用以下命令启动 Axon 服务器:

$ ./axonserver.jar

或者不运行 bash shell 时:

$ java -jar axonserver.jar

当您看到一条日志行宣布“Started Axon Server in some-value seconds (JVM running for some-other-value)”时,服务器已准备好采取行动。 要验证服务器是否正确启动,请打开页面 http://localhost:8024。

运行 Axon 服务器的替代方法

Axon 提供了 Axon Server 的 Docker 镜像,可在 DockerHub 上使用

或者,您可以使用此映像在 Docker 容器中运行 Axon Server 并将其部署到 Kubernetes。

有关更多详细信息,请参阅操作指南章节(部分:设置 Axon 服务器/启动)。

 

实现领域逻辑

命令处理

在本章中,我们将更详细地介绍在 Axon 应用程序中处理和分发命令的过程。 此处将涵盖聚合建模、外部命令处理程序、命令调度和测试等主题。 要更深入地解释什么是命令模型或聚合,我们建议阅读 DDD 和 CQRS 概念章节。

Aggregate

本章将介绍如何实现“聚合”的基础知识。 有关聚合是什么的更多详细信息,请阅读 DDD 和 CQRS 概念页面。

基本聚合结构

Aggregate 是一个常规对象,它包含状态和改变该状态的方法。 创建聚合对象时,您实际上是在创建“聚合根”,通常带有整个聚合的名称。 出于本说明的目的,将使用“礼品卡”域,这将礼品卡作为聚合(根)。 默认情况下,Axon 会将您的聚合配置为“事件来源”聚合(如此处所述)。 从今以后,我们的基本礼品卡聚合结构将侧重于事件采购方法:

import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.eventsourcing.EventSourcingHandler;
import org.axonframework.modelling.command.AggregateIdentifier;

import static org.axonframework.modelling.command.AggregateLifecycle.apply;

public class GiftCard {

    @AggregateIdentifier // 1.
    private String id;

    @CommandHandler // 2.
    public GiftCard(IssueCardCommand cmd) {
        // 3.
       apply(new CardIssuedEvent(cmd.getCardId(), cmd.getAmount()));
    }

    @EventSourcingHandler // 4.
    public void on(CardIssuedEvent evt) {
        id = evt.getCardId();
    }

    // 5.
    protected GiftCard() {
    }
    // omitted command handlers and event sourcing handlers
}

给定的代码片段中有几个值得注意的概念,标有编号的 Java 注释,指的是以下项目符号:

@AggregateIdentifier 是进入礼品卡聚合的外部参考点。 这个字段是一个硬性要求,因为没有它,Axon 将不知道给定命令的目标是哪个聚合。

@CommandHandler 注释的构造函数,或者以不同的方式放置“命令处理构造函数”。 此注释告诉框架给定的构造函数能够处理 IssueCardCommand。

@CommandHandler 注释函数是您放置决策/业务逻辑的地方。

静态 AggregateLifecycle#apply(Object...) 是在发布事件消息时使用的。 调用此函数后,提供的对象将在它们所应用的聚合范围内作为 EventMessage 发布。

使用@EventSourcingHandler 可以告诉框架,当聚合“源自其事件”时应调用带注释的函数。 由于所有事件源处理程序组合将形成聚合,这是所有状态更改发生的地方。 请注意,必须在聚合发布的第一个事件的 @EventSourcingHandler 中设置聚合标识符。 这通常是创建事件。 最后,@EventSourcingHandler 注释函数使用特定规则进行解析。 这些规则对于@EventHandler 带注释的方法是相同的,并且在带注释的事件处理程序中进行了彻底的解释。

Axon 需要的无参数构造函数。 Axon Framework 使用此构造函数创建一个空的聚合实例,然后使用过去的事件对其进行初始化。 未能提供此构造函数将导致加载 Aggregate 时发生异常。

消息处理函数的修饰符

事件处理程序方法可以是私有的,只要 JVM 的安全设置允许 Axon 框架更改方法的可访问性。 这允许您将聚合的公共 API(公开生成事件的方法)与处理事件的内部逻辑清楚地分开。

大多数 IDE 可以选择忽略带有特定注释的方法的“未使用的私有方法”警告。 或者,您可以向方法添加 @SuppressWarnings("UnusedDeclaration") 注释,以确保不会意外删除事件处理程序方法。

处理聚合中的命令

尽管命令处理程序可以放置在常规组件中(正如将在此处讨论的那样,建议直接在包含处理此命令的状态的聚合上定义命令处理程序。

要在聚合中定义命令处理程序,只需使用@CommandHandler 注释应该处理命令的方法。 @CommandHandler 注释的方法将成为命令消息的命令处理程序,其中命令名称与该方法的第一个参数的完全限定类名称匹配。 因此,使用@CommandHandler 注释的 void handle(RedeemCardCommand cmd) 方法签名将成为 RedeemCardCommand 命令消息的命令处理程序。

命令消息也可以用不同的命令名称发送。 为了能够正确处理这些,可以在 @CommandHandler 注释中指定 String commandName 值。

为了让 Axon 知道聚合类型的哪个实例应该处理命令消息,命令对象中携带聚合标识符的属性必须用@TargetAggregateIdentifier 进行注释。 注释可以放置在 Command 对象中的字段或访问器方法(例如 getter)上。

以 GiftCard 聚合为例,我们可以在聚合上识别出两个命令处理程序:

import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.modelling.command.AggregateIdentifier;

import static org.axonframework.modelling.command.AggregateLifecycle.apply;

public class GiftCard {

    @AggregateIdentifier
    private String id;
    private int remainingValue;

    @CommandHandler
    public GiftCard(IssueCardCommand cmd) {
        apply(new CardIssuedEvent(cmd.getCardId(), cmd.getAmount()));
    }

    @CommandHandler
    public void handle(RedeemCardCommand cmd) {
        if (cmd.getAmount() <= 0) {
            throw new IllegalArgumentException("amount <= 0");
        }
        if (cmd.getAmount() > remainingValue) {
            throw new IllegalStateException("amount > remaining value");
        }
        apply(new CardRedeemedEvent(id, cmd.getTransactionId(), cmd.getAmount()));
    }
    // omitted event sourcing handlers
}

GiftCard 处理的 Command 对象 IssueCardCommand 和 RedeemCardCommand 具有以下格式:

import org.axonframework.modelling.command.TargetAggregateIdentifier;

public class IssueCardCommand {

    @TargetAggregateIdentifier
    private final String cardId;
    private final Integer amount;

    public IssueCardCommand(String cardId, Integer amount) {
        this.cardId = cardId;
        this.amount = amount;
    }
    // omitted getters, equals/hashCode, toString functions
}

public class RedeemCardCommand {

    @TargetAggregateIdentifier
    private final String cardId;
    private final String transactionId;
    private final Integer amount;

    public RedeemCardCommand(String cardId, String transactionId, Integer amount) {
        this.cardId = cardId;
        this.transactionId = transactionId;
        this.amount = amount;
    }
    // omitted getters, equals/hashCode, toString functions
}

两个命令中存在的 cardId 是对 GiftCard 实例的引用,因此使用 @TargetAggregateIdentifier 批注进行批注。 创建聚合实例的命令不需要标识目标聚合标识符,因为尚不存在聚合。 尽管如此,为了一致性,还是建议在它们上注释聚合标识符。

如果您更喜欢使用其他机制来路由命令,则可以通过提供自定义 CommandTargetResolver 来覆盖该行为。 此类应根据给定命令返回聚合标识符和预期版本(如果有)。

聚合创建命令处理程序

当 @CommandHandler 注释被放置在聚合的构造函数上时,相应的命令将创建该聚合的新实例并将其添加到存储库中。 这些命令不需要针对特定的聚合实例。 因此,这些命令不需要任何 @TargetAggregateIdentifier 或 @TargetAggregateVersion 注释,也不会为这些命令调用自定义 CommandTargetResolver。

但是,无论命令的类型如何,只要您通过 Axon Server 等分发应用程序,强烈建议在给定消息上指定路由键。 @TargetAggregateIdentifier 也是如此,但是在没有值得注释的字段的情况下,应该添加 @RoutingKey 注释以确保可以路由命令。 此外,可以配置不同的 RoutingStrategy,如命令调度部分中进一步指定的那样。

业务逻辑和状态变化

在聚合内有一个特定的位置来执行业务逻辑验证和聚合状态更改。 命令处理程序应该决定聚合是否处于正确的状态。 如果是,则发布事件。 如果不是,则可能会忽略该命令或可能引发异常,具体取决于域的需要。

在任何命令处理函数中都不应发生状态更改。 事件源处理程序应该是更新聚合状态的唯一方法。 不这样做意味着当聚合从其事件中获取时,它会错过状态更改。

Aggregate Test Fixture 将防止命令处理功能中的无意状态更改。 因此,建议为任何聚合实现提供全面的测试用例。

何时处理事件

聚合需要的唯一状态是它做出决定所需的状态。 因此,仅当需要事件类似的状态更改来驱动未来验证时,才需要处理由聚合发布的事件。

应用来自事件源处理程序的事件

在某些情况下,尤其是当聚合结构增长到不仅仅是几个实体时,对在同一聚合的其他实体中发布的事件做出反应会更清晰(此处更详细地解释了多实体聚合)。 但是,由于在重建聚合状态时也会调用事件处理方法,因此必须采取特殊的预防措施。

可以在 Event Sourcing Handler 方法中 apply() 新事件。 这使得实体 'B' 可以应用事件来响应实体 'A' 做某事。 在获取给定聚合时重放历史事件时,Axon 将忽略 apply() 调用。 请注意,在从事件源处理程序发布事件消息的场景中,内部 apply() 调用的事件仅在所有实体都收到第一个事件后才发布到实体。 如果需要发布更多事件,根据应用内部事件后实体的状态,使用apply(...).andThenApply(...)。

对其他事件的反应

聚合不能处理来自其他来源的事件,而不是它自己。 这是有意为之,因为事件源处理程序用于重新创建聚合的状态。 为此,它只需要它自己的事件,因为这些事件代表它的状态变化。

为了让聚合对来自其他聚合实例的事件做出反应,应该利用 Sagas 或事件处理组件。

聚合生命周期操作

有几个操作需要在聚合的生命周期中执行。 为此,Axon 中的 AggregateLifecycle 类提供了几个静态函数:

apply(Object) 和 apply(Object, MetaData):AggregateLifecycle#apply 将在 EventBus 上发布一个事件消息,以便知道它源自执行操作的聚合。 有可能只提供事件对象,或者同时提供事件和一些特定的元数据。

createNew(Class, Callable):作为处理命令的结果实例化一个新的聚合。 阅读本文了解更多详情。

isLive():检查以验证聚合是否处于“活动”状态。 如果聚合已完成重放历史事件以重新创建其状态,则该聚合被认为是“活动的”。 如果聚合因此处于事件源的过程中,则 AggregateLifecycle.isLive() 调用将返回 false。 使用此 isLive() 方法,您可以执行仅应在处理新生成的事件时执行的活动。

markDeleted():将调用函数的聚合实例标记为“已删除”。 如果域指定给定的聚合可以被移除/删除/关闭,此后不再允许它处理任何命令,则很有用。 应该从@EventSourcingHandler 注释函数调用此函数,以确保被标记为已删除是该聚合状态的一部分。

多实体聚合

 复杂的业务逻辑通常需要的不仅仅是只有一个聚合根的聚合所能提供的。 在这种情况下,重要的是将复杂性分散到聚合内的多个“实体”上。 在本章中,我们将讨论在聚合中创建实体的细节以及它们如何处理消息。

实体间的状态

 对聚合不应公开状态的规则的一个常见误解是,任何实体都不应包含任何属性访问器方法。 情况并非如此。 事实上,如果聚合中的实体向同一聚合中的其他实体公开状态,聚合可能会受益很多。 但是,建议不要将状态暴露在聚合之外。

在“礼品卡”域中,本节定义了礼品卡聚合根。 让我们利用这个域来引入实体:

import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.modelling.command.AggregateMember;
import org.axonframework.modelling.command.EntityId;

public class GiftCard {

    @AggregateIdentifier
    private String id;

    @AggregateMember // 1.
    private List<GiftCardTransaction> transactions = new ArrayList<>();

    private int remainingValue;

    // omitted constructors, command and event sourcing handlers
}

public class GiftCardTransaction {

    @EntityId // 2.
    private String transactionId;

    private int transactionValue;
    private boolean reimbursed = false;

    public GiftCardTransaction(String transactionId, int transactionValue) {
        this.transactionId = transactionId;
        this.transactionValue = transactionValue;
    }

    public String getTransactionId() {
        return transactionId;
    }

    // omitted command handlers, event sourcing handlers and equals/hashCode
}

实体就像聚合根一样,是简单的对象,如新的 GiftCardTransaction 实体所示。 上面的代码片段展示了多实体聚合的两个重要概念:

1.声明子实体的字段必须使用@AggregateMember 进行注释。 这个注解告诉 Axon,注解的字段包含一个应该检查消息处理程序的类。

此示例显示了对 Iterable 实现的注释,但它也可以放置在单个对象或映射上。

在后一种情况下,Map 的值应该包含实体,而键包含一个用作它们的引用的值。

2.@EntityId 注释指定实体的标识字段。 需要能够将命令(或事件)消息路由到正确的实体实例。 将用于查找消息应路由到的实体的有效负载上的属性,默认为 @EntityId 注释字段的名称。

例如,在注释字段 transactionId 时,命令必须定义具有相同名称的属性,这意味着必须存在 transactionId 或 getTransactionId() 方法。

如果字段名称和路由属性不同,您可以使用@EntityId(routingKey = "customRoutingProperty") 显式提供一个值。

如果实体实现是子实体的集合或映射的一部分,则此注释是强制性的。

定义实体类型

集合或映射的字段声明应包含适当的泛型,以允许 Axon 识别集合或映射中包含的实体类型。 如果无法在声明中添加泛型(例如,因为您使用的是已经定义了泛型类型的自定义实现),则必须通过在 @AggregateMember 注释中指定类型字段来指定实体类型:

>@AggregateMember(type = GiftCardTransaction.class)。

实体中的命令处理

@CommandHandler 注释不限于聚合根。 将所有命令处理程序放在根中有时会导致聚合根上有大量方法,而其中许多方法只是将调用转发到底层实体之一。 如果是这种情况,您可以将 @CommandHandler 注释放在底层实体的方法之一上。 为了让 Axon 找到这些带注释的方法,在聚合根中声明实体的字段必须用 @AggregateMember 标记:

import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.modelling.command.AggregateMember;
import org.axonframework.modelling.command.EntityId;

import static org.axonframework.modelling.command.AggregateLifecycle.apply;

public class GiftCard {

    @AggregateIdentifier
    private String id;

    @AggregateMember
    private List<GiftCardTransaction> transactions = new ArrayList<>();

    private int remainingValue;

    // omitted constructors, command and event sourcing handlers

}

public class GiftCardTransaction {

    @EntityId
    private String transactionId;

    private int transactionValue;
    private boolean reimbursed = false;

    public GiftCardTransaction(String transactionId, int transactionValue) {
        this.transactionId = transactionId;
        this.transactionValue = transactionValue;
    }

    @CommandHandler
    public void handle(ReimburseCardCommand cmd) {
        if (reimbursed) {
            throw new IllegalStateException("Transaction already reimbursed");
        }
        apply(new CardReimbursedEvent(cmd.getCardId(), transactionId, transactionValue));
    }

    // omitted getter, event sourcing handler and equals/hashCode
}

请注意,仅检查注释字段的声明类型以查找命令处理程序。 如果在该实体的传入命令到达时字段值为 null,则会引发异常。 如果存在子实体的集合或映射,并且找不到与命令的路由键匹配的实体,则 Axon 会抛出 IllegalStateException,因为显然该聚合在该时间点无法处理该命令。

命令处理程序注意事项

请注意,每个命令在集合中必须只有一个处理程序。 这意味着您不能使用处理相同命令类型的 @CommandHandler 注释多个实体(无论是根还是非)。 如果您需要有条件地将命令路由到实体,这些实体的父级应该处理该命令,并根据适用的条件转发它。

字段的运行时类型不必完全是声明的类型。 但是,仅检查 @CommandHandler 方法的 @AggregateMember 注释字段的声明类型。

实体中的事件溯源处理程序

当使用事件源作为存储聚合的机制时,不仅聚合根需要使用事件来触发状态转换,聚合内的每个实体也需要使用事件来触发状态转换。 Axon 为事件溯源复杂的聚合结构提供了开箱即用的支持。

当实体(包括聚合根)应用事件时,它首先由聚合根处理,然后通过每个 @AggregateMember 注释字段向下冒泡到其包含的所有子实体:

import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.modelling.command.AggregateMember;
import org.axonframework.modelling.command.EntityId;

import static org.axonframework.modelling.command.AggregateLifecycle.apply;

public class GiftCard {

    @AggregateIdentifier
    private String id;
    @AggregateMember
    private List<GiftCardTransaction> transactions = new ArrayList<>();

    @CommandHandler
    public void handle(RedeemCardCommand cmd) {
        // Some decision making logic
        apply(new CardRedeemedEvent(id, cmd.getTransactionId(), cmd.getAmount()));
    }

    @EventSourcingHandler
    public void on(CardRedeemedEvent evt) {
        // 1.
        transactions.add(new GiftCardTransaction(evt.getTransactionId(), evt.getAmount()));
    }

    // omitted constructors, command and event sourcing handlers
}

public class GiftCardTransaction {

    @EntityId
    private String transactionId;

    private int transactionValue;
    private boolean reimbursed = false;

    public GiftCardTransaction(String transactionId, int transactionValue) {
        this.transactionId = transactionId;
        this.transactionValue = transactionValue;
    }

    @CommandHandler
    public void handle(ReimburseCardCommand cmd) {
        if (reimbursed) {
            throw new IllegalStateException("Transaction already reimbursed");
        }
        apply(new CardReimbursedEvent(cmd.getCardId(), transactionId, transactionValue));
    }

    @EventSourcingHandler
    public void on(CardReimbursedEvent event) {
        // 2.
        if (transactionId.equals(event.getTransactionId())) {
            reimbursed = true;
        }
    }

    // omitted getter and equals/hashCode
}

上面的代码片段中有两个细节值得一提,并用编号的 Java 注释指出:

1.实体的创建发生在其父级的事件源处理程序中。 因此,与聚合根一样,实体类上不可能有“命令处理构造函数”。

2.实体中的事件源处理程序执行验证检查接收到的事件是否确实属于该实体。

这是必要的,因为一个实体实例应用的事件也将由相同类型的任何其他实体实例处理。

通过更改 @AggregateMember 注释上的 eventForwardingMode,可以自定义第二点中描述的情况:

import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.modelling.command.AggregateMember;
import org.axonframework.modelling.command.ForwardMatchingInstances;

public class GiftCard {

    @AggregateIdentifier
    private String id;
    @AggregateMember(eventForwardingMode = ForwardMatchingInstances.class)
    private List<GiftCardTransaction> transactions = new ArrayList<>();

    // omitted constructors, command and event sourcing handlers
}

通过将 eventForwardingMode 设置为 ForwardMatchingInstances,仅当事件消息包含与实体上 @EntityId 注释字段的名称匹配的字段/getter 时,才会转发事件消息。 可以使用 @EntityId 注释上的 routingKey 字段进一步指定此路由行为,反映实体中路由命令的行为。 其他可以使用的转发模式有 ForwardAll(默认)和 ForwardNone,它们分别将所有事件转发给所有实体或根本不转发任何事件。

外部命令处理程序

命令处理函数通常直接放置在聚合上(如这里更详细的描述)。 然而,在某些情况下,不可能也不希望将命令直接路由到聚合实例。 然而,消息处理函数,如命令处理程序,可以放置在任何对象上。 因此可以实例化“命令处理对象”。

命令处理对象是一个简单(常规)对象,它具有 @CommandHandler 注释方法。 与聚合不同,命令处理对象只有一个实例,它处理它在其方法中声明的类型的所有命令:

import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.modelling.command.Repository;

public class GiftCardCommandHandler {

    // 1.
    private final Repository<GiftCard> giftCardRepository;

    @CommandHandler
    public void handle(RedeemCardCommand cmd) {
        giftCardRepository.load(cmd.getCardId()) // 2.
                          .execute(giftCard -> giftCard.handle(cmd)); // 3.
    }

    // omitted constructor
}

在上面的代码片段中,我们决定不再直接在礼品卡上处理 RedeemCardCommand。 相反,我们手动加载 GiftCard 并在其上执行所需的方法:

1.礼品卡聚合的存储库,用于检索和存储聚合。 如果@CommandHandler 方法直接放在聚合上,Axon 将自动知道调用存储库来加载给定的实例。

因此,直接访问存储库不是强制性的,而是一种设计选择

2.要加载预期的礼品卡聚合实例,请使用 Repository#load(String) 方法。 提供的参数应该是聚合标识符。

3.加载该聚合后,应调用 Aggregate#execute(Consumer) 函数对聚合执行操作。

使用 execute 函数确保正确启动 Aggregate 生命周期。

状态存储聚合

在聚合主页中,我们已经看到了如何创建由事件溯源支持的聚合。 换句话说,事件源聚合的存储方法是通过重放构成聚合变化的事件。

然而,聚合也可以按原样存储。 这样做时,用于保存和加载聚合的存储库是 GenericJpaRepository。 状态存储聚合的结构与事件源聚合略有不同:

import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.eventhandling.EventHandler;
import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.modelling.command.AggregateMember;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;

@Entity // 1.
public class GiftCard {

    @Id // 2.
    @AggregateIdentifier
    private String id;

    // 3.
    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinColumn(name = "giftCardId")
    @AggregateMember
    private List<GiftCardTransaction> transactions = new ArrayList<>();

    private int remainingValue;

    @CommandHandler  // 4.
    public GiftCard(IssueCardCommand cmd) {
        if (cmd.getAmount() <= 0) {
            throw new IllegalArgumentException("amount <= 0");
        }
        id = cmd.getCardId();
        remainingValue = cmd.getAmount();

         // 5.
        apply(new CardIssuedEvent(cmd.getCardId(), cmd.getAmount()));
    }

    @CommandHandler
    public void handle(RedeemCardCommand cmd) {
         // 6.
        if (cmd.getAmount() <= 0) {
            throw new IllegalArgumentException("amount <= 0");
        }
        if (cmd.getAmount() > remainingValue) {
            throw new IllegalStateException("amount > remaining value");
        }
        if (transactions.stream().map(GiftCardTransaction::getTransactionId).anyMatch(cmd.getTransactionId()::equals)) {
            throw new IllegalStateException("TransactionId must be unique");
        }

         // 7.
        remainingValue -= cmd.getAmount();
        transactions.add(new GiftCardTransaction(id, cmd.getTransactionId(), cmd.getAmount()));

        apply(new CardRedeemedEvent(id, cmd.getTransactionId(), cmd.getAmount()));
    }

    @EventHandler  // 8.
    protected void on(CardReimbursedEvent event) {
        this.remainingValue += event.getAmount();
    }

    protected GiftCard() { }  // 9.
}

上面的摘录显示了“礼品卡服务”中存储的聚合状态。 代码段中的编号注释指出了 Axon 的具体细节,在此处进行了解释:

1.由于聚合存储在 JPA 存储库中,因此需要使用 @Entity 对类进行注释。

2.聚合根必须声明一个包含聚合标识符的字段。 最迟必须在发布第一个事件时初始化此标识符。 此标识符字段必须由 @AggregateIdentifier 注释进行注释。 使用JPA存储Aggregate时,Axon知道使用JPA提供的@Id注解。 由于 Aggregate 是一个实体,@Id 注释是一个硬性要求。

3.这个聚合有几个“聚合成员”。 由于聚合按原样存储,因此应考虑实体的正确映射。

4.@CommandHandler 注释的构造函数,或者以不同的方式放置“命令处理构造函数”。 此注释告诉框架给定的构造函数能够处理 IssueCardCommand。

5.静态 AggregateLifecycle#apply(Object...) 可用于发布事件消息。 调用此函数后,提供的对象将在它们所应用的聚合范围内作为 EventMessage 发布。

6.命令处理方法将首先决定此时传入的命令是否有效处理。

7.验证业务逻辑后,可以调整Aggregate的状态

8.Aggregate 中的实体可以通过定义 @EventHandler 注释方法来侦听 Aggregate 发布的事件。

在任何外部处理程序处理之前发布事件消息时,将调用这些方法。

9.一个无参数构造函数,这是 JPA 所需的。 未能提供此构造函数将导致加载 Aggregate 时发生异常。

在命令处理程序中调整状态

与事件源聚合不同,状态存储聚合可以将决策逻辑和命令处理程序中的状态更改配对。 遵循此范例对状态存储聚合没有任何影响,因为没有驱动其状态的事件源处理程序。

调度命令

聚合和外部命令处理程序页面提供了有关如何在应用程序中处理命令消息的背景。 调度过程是这种命令消息的起点。 Axon 提供了两个可用于将命令发送到命令处理程序的接口,它们是:

1.命令总线,以及

2.命令网关

此页面将显示如何以及何时使用命令网关和总线。 此处讨论了如何配置命令网关和总线实现的细节

命令总线

“命令总线”是将命令分派到各自的命令处理程序的机制。 因此,基础设施组件知道哪个组件可以处理哪个命令。

每个命令总是发送到一个命令处理程序。 如果没有可用于分派命令的命令处理程序,则会引发 NoHandlerForCommandException 异常。

CommandBus 提供两种方法将命令分派到各自的处理程序,即 dispatch(CommandMessage) 和 dispatch(CommandMessage, CommandCallback) 方法:

private CommandBus commandBus; // 1.

public void dispatchCommands() {
    String cardId = UUID.randomUUID().toString(); // 2.

    // 3. & 4.
    commandBus.dispatch(GenericCommandMessage.asCommandMessage(new IssueCardCommand(cardId, 100, "shopId")));

    // 5. & 6.
    commandBus.dispatch(
            GenericCommandMessage.asCommandMessage(new IssueCardCommand(cardId, 100, "shopId")),
            (CommandCallback<IssueCardCommand, String>) (cmdMsg, cmdResultMsg) -> {
                // 7.
                if (cmdResultMsg.isExceptional()) {
                    Throwable throwable = cmdResultMsg.exceptionResult();
                } else {
                    String commandResult = cmdResultMsg.getPayload();
                }
            }
    );
}
// omitted class, constructor and result usage

上面描述的 CommandDispatcher 举例说明了调度命令的几个重要方面和功能:

1.CommandBus 接口提供发送命令消息的功能。

2.根据最佳实践,聚合标识符被初始化为随机唯一标识符的字符串。 类型标识符对象也是可能的,只要该对象实现了一个合理的 toString() 函数。

3.GenericCommandMessage#asCommandMessage(Object) 方法用于创建 CommandMessage。 为了能够在 CommandBus 上分派命令,您需要将自己的命令对象(例如“命令消息有效负载”)包装在 CommandMessage 中。

CommandMessage 还允许将元数据添加到命令消息中。

4.CommandBus#dispatch(CommandMessage) 函数将在总线上分派提供的 CommandMessage,以便传递给命令处理程序。

如果应用程序对命令的结果不直接感兴趣,则可以使用此方法。

5.如果命令处理的结果与您的应用程序相关,则可以提供可选的第二个参数,即 CommandCallback。 CommandCallback 允许在命令处理完成时通知调度组件。

6.Command Callback 有一个函数 onResult(CommandMessage, CommandResultMessage),它在命令处理完成时被调用。 第一个参数是调度命令,第二个参数是调度命令的执行结果。

最后,CommandCallback 是一个“功能接口”,因为 onResult 是它的唯一方法。 因此, commandBus.dispatch(commandMessage, (cmdMsg, commandResultMessage) -> { /* ... */ }) 也是可能的。

7.CommandResultMessage 提供 API 来验证命令执行是异常还是成功。

如果 CommandResultMessage#isExceptional 返回 true,您可以假设 CommandResultMessage#exceptionResult 将返回一个包含实际异常的 Throwable 实例。 否则,CommandResultMessage#getPayload 方法可能会为您提供实际结果或 null,如此处进一步指定的那样。

命令回调注意事项

在使用 dispatch(CommandMessage, CommandCallback) 的情况下,调用组件可能不会假设回调是在调度命令的同一线程中调用的。 如果调用线程在继续之前依赖于结果,则可以使用 FutureCallback。 FutureCallback 是 Future(在 java.concurrent 包中定义)和 Axon 的 CommandCallback 的组合。 或者,考虑使用 CommandGateway。

命令网关

“命令网关”是一种用于调度命令的便捷方法。 它通过在 CommandBus 上分派命令时为您抽象某些方面来实现。 它使用下面的命令总线来执行消息的实际调度。 虽然您不需要使用网关来调度命令,但它通常是最简单的选择。

CommandGateway 接口可以分为两组方法,即 send 和 sendAndWait:

private CommandGateway commandGateway; // 1.

public void sendCommand() {
    String cardId = UUID.randomUUID().toString(); // 2.

    // 3.
    CompletableFuture<String> futureResult = commandGateway.send(new IssueCardCommand(cardId, 100, "shopId"));
}
// omitted class, constructor and result usage

如上所示的发送 API 引入了几个概念,用编号的注释标记:

1.CommandGateway 接口提供发送命令消息的功能。 它通过在内部利用 CommandBus 接口调度消息来实现。

2.根据最佳实践,聚合标识符被初始化为随机唯一标识符的字符串。 类型标识符对象也是可能的,只要该对象实现了一个合理的 toString() 函数。

3.send(Object) 函数需要一个参数,即命令对象。 这是调度命令的异步方法。 因此,send 方法的响应是 CompletableFuture。 这允许在返回命令结果后链接后续操作。

使用 send(Object) 时的回调

CommandGateway#send(Object) 方法在幕后使用 FutureCallback 来从命令处理线程中解除命令调度线程的阻塞。

通过使用 sendAndWait 方法,还可以实现发送消息的同步方法:

private CommandGateway commandGateway;

public void sendCommandAndWaitOnResult() {
    IssueCardCommand commandPayload = new IssueCardCommand(UUID.randomUUID().toString(), 100, "shopId");
    // 1.
    String result = commandGateway.sendAndWait(commandPayload);

    // 2.
    result = commandGateway.sendAndWait(commandPayload, 1000, TimeUnit.MILLISECONDS);
}
// omitted class, constructor and result usage

1.CommandGateway#sendAndWait(Object) 函数接受一个参数,即您的命令对象。 它将无限期地等待,直到命令调度和处理过程得到解决。 此方法返回的结果可以是成功的,也可以是异常的,这将在此处进行解释。

2.如果不希望无限期地等待,则可以在命令对象旁边提供与“时间单位”配对的“超时”。 这样做将确保命令分派线程不会等待超过指定的时间。 如果在使用此方法时命令调度/处理被中断或达到超时,则命令结果将为空。 在所有其他场景中,结果遵循引用的方法。

命令调度结果

一般而言,调度命令将有两种可能的结果:

1.命令处理成功,以及

2.命令处理异常

结果在某种程度上取决于调度过程,但更多取决于命令处理程序的实现。 因此,如果@CommandHandler 注释的函数由于某些业务逻辑而引发异常,则该异常将是调度命令的结果。

命令处理的成功解析不应提供任何返回对象。 因此,如果 CommandBus/CommandGateway 提供响应(直接或通过 CommandResultMessage),那么您应该假设成功的命令处理结果返回 null。

虽然可以从命令处理程序返回结果,但这应该很少使用。 命令的意图永远不应该是检索值,因为这表明消息应该被设计为查询消息。 例外情况是聚合根的标识符或聚合根已实例化的实体的标识符。 该框架在聚合的@CommandHandler 注释构造函数上内置了一个这样的异常。 如果“命令处理构造函数”已成功执行,将返回 @AggregateIdentifier 注释字段的值而不是聚合本身。

 

测试

CQRS 的好处之一,尤其是事件溯源的好处,是可以纯粹根据事件和命令来表达测试。 作为功能组件,事件和命令对领域专家或业务所有者都具有明确的意义。 这不仅意味着以事件和命令表示的测试具有明确的功能意义,还意味着它们几乎不依赖于任何实现选择。

本章介绍的功能需要axon-test模块,可以通过配置maven依赖(使用<artifactId>axon-test</artifactId>和<scope>test</scope>)或者从完整包下载中获取 .

本章中描述的装置适用于任何测试框架,例如 JUnit 和 TestNG。

命令模型测试

命令处理组件通常是任何基于 CQRS 的架构中最复杂的组件。 由于比其他组件更复杂,这也意味着该组件有额外的测试相关要求。

尽管更复杂,但命令处理组件的 API 相当简单。 它有一个命令进来,事件出去。 在某些情况下,可能会有查询作为命令执行的一部分。 除此之外,命令和事件是 API 的唯一部分。 这意味着可以根据事件和命令完全定义测试场景。 通常,形状为:

鉴于过去发生的某些事件,

执行此命令时,

期望这些事件被发布和/或存储

Axon Framework 提供了一个测试装置,可让您做到这一点。 AggregateTestFixture 允许您配置由必要的命令处理程序和存储库组成的特定基础结构,并根据“given-when-then”事件和命令表达您的场景。

Focus of a Test Fixture

由于此处的测试单元是聚合,因此 AggregateTestFixture 仅用于测试一个聚合。 因此,when(或给定)子句中的所有命令都旨在针对测试fixture中的聚合。 此外,所有给定和预期的事件都应该从被测设备的聚合触发。

以下示例显示了“given-when-then”测试装置在 GiftCard 聚合(如之前定义)上的 JUnit 4 的使用:

import org.axonframework.test.aggregate.AggregateTestFixture;
import org.axonframework.test.aggregate.FixtureConfiguration;
 
public class GiftCardTest {
 
private FixtureConfiguration<GiftCard> fixture;
 
@Before
public void setUp() {
fixture = new AggregateTestFixture<>(GiftCard.class);
}
 
@Test
public void testRedeemCardCommand() {
fixture.given(new CardIssuedEvent("cardId", 100))
.when(new RedeemCardCommand("cardId", "transactionId", 20))
.expectSuccessfulHandlerExecution()
.expectEvents(new CardRedeemedEvent("cardId", "transactionId", 20));
/*
These four lines define the actual scenario and its expected result.
The first line defines the events that happened in the past.
These events define the state of the aggregate under test.
In practical terms, these are the events that the event store returns
when an aggregate is loaded.
 
The second line defines the command that we wish to execute against our system.
 
Finally, we have two more methods that define expected behavior.
In the example, we use the recommended void return type.
The last method defines that we expect a single event as result
of the command execution.
*/
}
}

 

“given-when-then”测试装置定义了三个阶段:配置、执行和验证。 每个阶段都由不同的接口表示:分别是 FixtureConfiguration、TestExecutor 和 ResultValidator。

Fluent Interface

为了优化利用这些阶段之间的迁移,最好使用这些方法提供的流畅接口,如上例所示。

测试设置

在配置阶段(即在提供第一个“给定”之前),您提供执行测试所需的构建块。 默认情况下,事件总线、命令总线和事件存储的专用版本作为装置的一部分提供。 有适当的访问器方法来获取对它们的引用。 任何未直接在聚合上注册的命令处理程序都需要使用 registerAnnotatedCommandHandler 方法进行显式配置。 除了带注释的命令处理程序之外,您还可以注册各种组件和设置,这些组件和设置定义了应如何设置围绕聚合测试的基础设施,包括以下内容:

注册库: 注册自定义聚合存储库。

注册存储库提供者: 注册一个用于生成新聚合的 RepositoryProvider。

注册聚合工厂: 注册一个自定义 AggregateFactory。

registerAnnotatedCommandHandler: 注册一个带注释的命令处理程序对象。

注册命令处理程序: 注册 CommandMessage 的 MessageHandler。

注册可注射资源: 注册一个可以注入消息处理成员的资源。

registerCommandDispatchInterceptor: 注册一个命令 MessageDispatchInterceptor。

registerCommandHandlerInterceptor: 注册一个命令 MessageHandlerInterceptor。

registerDeadlineDispatchInterceptor: 注册一个 DeadlineMessage MessageDispatchInterceptor。

registerDeadlineHandlerInterceptor: 注册一个 DeadlineMessage MessageHandlerInterceptor。

注册字段过滤器: 注册在“then”阶段比较对象时使用的字段过滤器。

注册忽略字段: 注册一个在执行状态相等时应该被给定类忽略的字段。

registerHandler 定义: 将自定义 HandlerDefinition 注册到测试装置。

registerCommandTargetResolver: 将 CommandTargetResolver 注册到测试装置。

一旦配置了灯具,您就可以定义“给定”事件。 测试装置将这些事件包装为 DomainEventMessages。 如果“给定”事件实现了消息,则该消息的有效载荷和元数据将包含在 DomainEventMessage 中,否则将给定事件用作有效载荷。 DomainEventMessage 的序列号是连续的,从 0 开始。如果没有预期的先前活动,则可以使用 givenNoPriorActivity() 作为起点。

或者,您也可以将命令作为“给定”场景提供。 在这种情况下,这些命令生成的事件将用于在执行实际测试命令时作为聚合的事件源。 使用“givenCommands(...)”方法提供命令对象。

 

“给定”阶段的最后一个选项是直接提供聚合的状态。 在事件溯源的情况下不建议这样做,并且仅在基于命令或事件重建聚合不可行或使用状态存储聚合的情况下。 使用 fixture.givenState(() -> new GiftCard()) 来定义初始状态。

测试执行阶段

执行阶段允许您进入验证阶段的两个入口点。 首先,您可以提供要针对命令处理组件执行的命令。 与给定的事件类似,如果提供的 Command 是 CommandMessage 类型,它将按原样分派。 被调用的处理程序的行为(在聚合上或作为外部处理程序)受到监视,并与在验证阶段注册的期望进行比较。

其次,可以使用 whenThenTimeElapses(Duration) 和 whenThenTimeAdvancesTo(Instant) 句柄经过一定的时间跨度。 这些支持测试 DeadlineMessages 的发布,如本章进一步定义的那样。

请注意,仅监控在测试执行阶段发生的活动。 验证阶段不考虑在“给定”阶段产生的任何事件或副作用。

非法状态变化检测

在测试执行期间,Axon 尝试检测被测聚合中的任何非法状态变化。 它通过将命令执行后的聚合状态与来自所有“给定”和存储事件的聚合状态进行比较来实现。 如果该状态不相同,则意味着在聚合的事件处理程序方法之外发生了状态更改。 比较中会忽略静态和瞬态字段,因为它们通常包含对资源的引用。

您可以使用 setReportIllegalStateChange() 方法在装置的配置中切换检测。

验证阶段

最后一个阶段是验证阶段,它允许您检查命令处理组件的活动。 这通常纯粹是根据返回值和事件来完成的。

验证命令结果

测试装置允许您验证命令处理程序的返回值。 您可以明确定义预期的返回值,或者简单地要求方法成功返回。 您还可以表达您希望 CommandHandler 抛出的任何异常。

以下方法可用于验证命令结果:

fixture.expectSuccessfulHandlerExecution(): 验证处理程序是否返回了未标记为异常响应的常规响应。

不评估确切的响应。

fixture.expectResultMessagePayload(Object): 验证处理程序返回成功的响应,有效负载等于给定的有效负载。

fixture.expectResultMessagePayloadMatching(Matcher): 验证处理程序返回成功的响应,有效负载与给定的匹配器匹配

fixture.expectResultMessage(CommandResultMessage): 验证收到的 CommandResultMessage 具有与给定消息相同的有效载荷和元数据。

fixture.expectResultMessageMatching(Matcher): 验证 CommandResultMessage 与给定的匹配器匹配。

fixture.expectException(Matcher): 验证命令处理结果是异常结果,并且该异常与给定的 Matcher 匹配。

fixture.expectException(Class): 验证命令处理结果是具有给定异常类型的异常结果。

fixture.expectExceptionMessage(String): 验证命令处理结果是异常结果并且异常消息等于给定消息。

fixture.expectExceptionMessage(Matcher): 验证命令处理结果是异常结果并且异常消息与给定的匹配器匹配。

验证已发布的事件

另一个组件是已发布事件的验证。 有两种匹配预期事件的方法。

首先是传入需要与实际事件进行字面比较的事件实例。 将预期事件的所有属性与实际事件中的对应属性进行比较(使用 equals())。 如果其中一个属性不相等,则测试失败并生成大量错误报告。

表达期望的另一种方式是使用“匹配器”(由 Hamcrest 库提供)。 Matcher 是一个接口,它规定了两个方法:matches(Object) 和 describeTo(Description)。 第一个返回一个布尔值来指示匹配器是否匹配。 第二个允许你表达你的期望。 例如,“GreaterThanTwoMatcher”可以将“值大于 2 的任何事件”附加到描述中。 描述允许创建关于测试用例失败原因的表达错误消息。

为事件列表创建匹配器可能是乏味且容易出错的工作。 为了简化事情,Axon 提供了一组匹配器,允许您提供一组特定于事件的匹配器并告诉 Axon 它们应该如何与列表匹配。 这些匹配器通过抽象的 Matchers 实用程序类静态可用。

以下是可用事件列表匹配器及其用途的概述:

List with all of: Matchers.listWithAllOf(event matchers...) 如果所有提供的事件匹配器与实际事件列表中的至少一个事件匹配,则此匹配器将成功。

多个匹配器是否匹配同一个事件并不重要, 也不是列表中的事件与任何匹配器不匹配。

List with any of: Matchers.listWithAnyOf(event matchers...) 如果提供的一个或多个事件匹配器与一个或多个匹配器匹配,则此匹配器将成功 实际事件列表中的事件。

一些匹配器甚至可能根本不匹配,而另一个匹配多个其他匹配器。

Sequence of Events:Matchers.sequenceOf(event matchers...) 使用此匹配器来验证实际事件是否按照与提供的事件匹配器相同的顺序进行匹配。如果每个匹配器匹配前一个匹配器匹配的事件之后的事件,则它会成功。这意味着可能会出现不匹配事件的“差距”。

如果在评估事件后有更多匹配器可用,则它们都与“null”匹配。由事件匹配器决定他们是否接受。

Exact sequence of Events: Matchers.exactSequenceOf(event matchers...) “事件序列”匹配器的变体,其中不允许出现不匹配事件的间隙。 这意味着每个匹配器必须直接匹配前一个匹配器匹配的事件之后的事件。

为方便起见,提供了一些常用的事件匹配器。 它们与单个事件实例匹配:

Equal event:Matchers.equalTo(instance...) 验证给定对象在语义上等于给定事件。 此匹配器将使用空安全的 equals 方法比较实际和预期对象字段中的所有值。

这意味着可以比较事件,即使它们没有实现 equals 方法。 存储在给定参数字段中的对象使用等号进行比较, 要求他们正确实施。

No more events:Matchers.andNoMore() 或 Matchers.nothing() 仅匹配空值。

可以将此匹配器作为最后一个匹配器添加到事件匹配器的确切序列中,以确保不存在未匹配的事件。

Predicate Matching:Matchers.matches(Predicate) 或 Matchers.predicate(Predicate) 创建与指定 Predicate 定义的值匹配的 Matcher。 可以在 Predicate API 提供更好的方法来验证结果的情况下使用。

由于匹配器会传递一个事件消息列表,因此您有时只想验证消息的有效负载。 有匹配器可以帮助您:

Payload matching:Matchers.messageWithPayload(payload matcher) 验证消息的有效负载是否与给定的有效负载匹配器匹配。

Payloads matching:Matchers.payloadsMatching(list matcher) 验证消息的有效负载是否与给定的匹配器匹配。 给定的匹配器必须与包含每个消息有效负载的列表匹配。 有效载荷匹配匹配器通常用作外部匹配器以防止有效载荷匹配器的重复。

下面是一个显示这些匹配器用法的小代码示例。 在这个例子中,我们期望发布两个事件。 第一个事件必须是“ThirdEvent”,第二个事件必须是“aFourthEventWithSomeSpecialThings”。 可能没有第三个事件,因为它会在“andNoMore”匹配器中失败。

import org.axonframework.test.aggregate.FixtureConfiguration;
 
import static org.axonframework.test.matchers.Matchers.andNoMore;
import static org.axonframework.test.matchers.Matchers.equalTo;
import static org.axonframework.test.matchers.Matchers.exactSequenceOf;
import static org.axonframework.test.matchers.Matchers.messageWithPayload;
import static org.axonframework.test.matchers.Matchers.payloadsMatching;
 
class MyCommandModelTest {
 
private FixtureConfiguration<MyCommandModel> fixture;
 
public void testWithMatchers() {
fixture.given(new FirstEvent(), new SecondEvent())
.when(new DoSomethingCommand("aggregateId"))
.expectEventsMatching(exactSequenceOf(
// we can match against the payload only:
messageWithPayload(equalTo(new ThirdEvent())),
// this will match against a Message
aFourthEventWithSomeSpecialThings(),
// this will ensure that there are no more events
andNoMore()
));
 
// or if we prefer to match on payloads only:
.expectEventsMatching(payloadsMatching(
exactSequenceOf(
// we only have payloads, so we can equalTo directly
equalTo(new ThirdEvent()),
// now, this matcher matches against the payload too
aFourthEventWithSomeSpecialThings(),
// this still requires that there is no more events
andNoMore()
)
));
}
}
 
验证聚合状态
在某些情况下,可能需要验证测试后聚合体的状态。 given-when-then场景中尤其如此,其中给定也代表初始状态,这在使用状态存储聚合时很常见。
Fixture 提供了一种方法,允许验证聚合的状态,因为它在执行阶段(例如,when 状态)之后留下来进行验证。
fixture.givenState(() -> new GiftCard())
       .when(new RedeemCardCommand())
       .expectState(state -> {
           // perform assertions
       });
expectState 方法采用 Aggregate 类型的使用者。 使用测试框架提供的常规断言来断言给定聚合的状态。 任何(运行时)异常或错误都会导致测试用例失败。
事件源聚合状态验证
测试事件源聚合的状态验证被认为是不好的做法。 理想情况下,聚合的状态对测试代码是完全不透明的,因为只应验证行为。 通常,验证状态的愿望表明测试套件中缺少某个测试场景。
验证截止日期
验证阶段还提供了验证给定聚合实例的预定和满足最后期限的选项。 您可以通过 Duration 或 Instant 来预期预定的截止日期,使用显式等于、匹配器或仅使用截止日期类型来验证截止日期消息。 以下方法可用于验证截止日期:
expectScheduledDeadline(DurationObject):
明确期望在指定的 Duration 之后安排给定的截止日期。
expectScheduledDeadlineMatching(Duration, Matcher):
期望在指定的 Duration 之后安排与 Matcher 匹配的截止日期。
expectScheduledDeadlineOfType(Duration, Class):
期望在指定的 Duration 之后安排与给定类型匹配的截止日期。
expectScheduledDeadline(InstantObject):
明确期望在指定的 Instant 安排给定的截止日期。
expectScheduledDeadlineMatching(Instant, Matcher):
期望在指定的 Instant 安排与 Matcher 匹配的截止日期。
expectScheduledDeadlineOfType(Instant, Class):
期望在指定的 Instant 安排与给定类型匹配的截止日期。
expectNoScheduledDeadlines():
预计根本没有安排截止日期。
expectDeadlinesMet(Object...):
明确期望已达到一个或多个最后期限。
expectDeadlinesMetMatching(Matcher<List<DeadlineMessage>>):
预计已达到匹配的截止日期或多个匹配的截止日期。
 
从另一个聚合创建聚合
通常,实例化一个新的聚合是通过发出一个创建命令来完成的,该命令由@CommandHandler 注释的聚合构造函数处理。 例如,此类命令可以由简单的 REST 端点或事件处理组件发布,作为对特定事件的反应。 然而,有时域描述了从另一个实体创建的某些实体。 在这种情况下,从它的父聚合实例化聚合会更忠实于域。
聚合自聚合用例
从“父”聚合创建“子”聚合的最合适场景是当创建子聚合的决定位于父聚合的上下文中时。 例如,如果父聚合包含可以驱动此子创建决策的必要状态,则这可以表现出来。
如何从另一个聚合创建聚合
让我们假设我们有一个 ParentAggregate,在处理某个命令时将决定创建一个 ChildAggregate。 为了实现这一点,ParentAggregate 看起来像这样:
import org.axonframework.commandhandling.CommandHandler;

import static org.axonframework.modelling.command.AggregateLifecycle.createNew;

public class ParentAggregate {

    @CommandHandler
    public void handle(SomeParentCommand command) {
        createNew(
            ChildAggregate.class,
            () -> new ChildAggregate(/* provide required constructor parameters if applicable */)
        );
    }
    // omitted no-op constructor, event sourcing handlers and other command handlers
}
AggregateLifecycle#createNew(Class<T>, Callable<T>) 是实例化另一个 Aggregate 的关键,比如我们的 ChildAggregate 作为处理命令的反应。 createNew 方法的第一个参数是要创建的聚合的类。 第二个参数是工厂方法,它期望结果是与给定类型相同的对象。
ChildAggregate 实现在这种情况下将类似于以下格式:
import static org.axonframework.modelling.command.AggregateLifecycle.apply;

public class ChildAggregate {

    public ChildAggregate(String aggregateId) {
        apply(new ChildAggregateCreatedEvent(aggregateId));
    }
    // omitted no-op constructor, command and event sourcing handlers
}
请注意,ChildAggregateCreatedEvent 显式应用于通知 ChildAggregate 已创建,否则此知识将包含在 ParentAggregate 的 SomeParentCommand 命令处理程序中。
从事件源处理程序创建聚合?
新聚合的创建应该在命令处理程序中完成,而不是在事件源处理程序中完成。 这背后的基本原理是,当父聚合源自其事件时,您不希望创建新的子聚合,因为这会不合需要地创建新的子聚合实例
但是,如果在事件源处理程序中意外调用了 createNew 方法,则将抛出 UnsupportedOperationException 作为停止间隙解决方案。
 
解决冲突
明确更改的含义的主要优点之一是您可以更精确地检测冲突的更改。 通常,当两个用户(几乎)同时对同一数据进行操作时,就会发生这些冲突的更改。 想象一下,两个用户都在查看特定版本的数据。 他们都决定对该数据进行更改。 他们都会发送一个命令,比如“在这个聚合的版本 X 上,做那个”,其中 X 是聚合的预期版本。 其中之一会将更改实际应用于预期版本。 其他用户不会。
当聚合被另一个进程修改时,不是简单地拒绝所有传入的命令,您可以检查用户的意图是否与任何看不见的更改冲突。
要检测冲突,请将 ConflictResolver 类型的参数传递给聚合的 @CommandHandler 方法。 此接口提供了 detectConflicts 方法,允许您定义在执行特定类型的命令时被视为冲突的事件类型。
预期的聚合版本
请注意,如果聚合加载了预期版本,则 ConflictResolver 将仅包含任何潜在的冲突事件。 在命令的字段上使用 @TargetAggregateVersion 以指示聚合的预期版本。
如果找到与predicate匹配的事件,则会抛出异常(detectConflicts 的可选第二个参数允许您定义要抛出的异常)。 如果未找到,则处理照常继续。
如果没有调用detectConflicts,并且存在潜在的冲突事件,@CommandHandler 将失败。 当提供了预期的版本,但在@CommandHandler 方法的参数中没有可用的 ConflictResolver 时,可能会出现这种情况。
 
事件处理
事件处理程序是作用于传入事件的组件。 它们通常根据命令模型做出的决定来执行逻辑。 通常,这涉及更新视图模型或将更新转发到其他组件,例如第三方集成。 在某些情况下,事件处理程序会根据它们接收到的事件(的模式)自己抛出事件,甚至发送命令来触发进一步的更改。
posted on 2021-08-16 22:12  渐行渐远的那些人  阅读(805)  评论(0编辑  收藏  举报