本节将解释如何获取Axon的二进制文件以开始使用。目前有两种方法:从我们的网站下载二进制文件或为您的构建系统配置存储库(Maven,Gradle等)。
下载Axon
您可以从我们的下载页面下载Axon Framework。
此页面提供了许多下载。通常,您会想要使用最新的稳定版本。但是,如果您渴望开始使用最新且最强大的功能,则可以考虑使用快照版本。下载页面包含许多可供下载的程序集。其中一些仅提供Axon库本身,而另一些则提供Axon依赖的库。还有一个“完整的”zip文件,其中包含了Axon及其依赖项,来源和文档,所有文件都在一次下载中。
如果你真的想保持处于开发的前沿,你可以克隆Git仓库:git://github.com/AxonFramework/AxonFramework.git,或者访问https://github.com/AxonFramework/AxonFramework来浏览在线的源代码。
配置Maven
如果您使用maven作为构建工具,则需要为项目配置正确的依赖关系。 在您的依赖项部分添加以下代码:
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-core</artifactId>
<version>${axon.version</version>
</dependency>
Axon框架提供的大部分功能都是可选的,并且需要额外的依赖关系。 我们选择不默认添加这些依赖关系,因为它们可能会使用您不需要的构件给您的项目添乱。
基础设施要求
Axon框架不会对基础架构施加很多要求。 它已经针对Java 8进行了构建和测试,或多或少成为唯一的要求。由于Axon本身不创建任何连接或线程,因此在Application Server上运行是安全的。 Axon通过使用Executor来抽象所有异步行为,例如,您可以很容易地传递容器管理的Thread Pool作为参数。 如果您没有使用完整的应用程序服务器(例如Tomcat,Jetty或独立应用程序),则可以使用Executors类或SpringFramework来创建和配置线程池。
当你遇到问题时
在实施您的应用程序时,您可能遇到问题,想知道某些情况的发生方式,或者有一些问题需要答案。 Axon用户邮件列表可以帮助您。 只需发送电子邮件至[email protected]。其他用户以及Axon Framework的贡献者可以帮助解决您的问题。如果发现错误,可以通过https://github.com/AxonFramework/AxonFramework/issues报告。报告问题时,请确保清楚地描述问题。解释你做了什么,结果是什么以及你期望发生什么。如果可能,请提供一个非常简单的单元测试(JUnit)来显示问题。 这使得修复更简单。
为Axon框架做出贡献
Axon框架的开发从未完成。 我们希望在我们的框架中包含更多的功能,以便继续开发可扩展的可扩展应用程序。这意味着我们不断寻求帮助开发我们的框架。您可以通过多种方式为Axon框架做出贡献:
- 您可以在我们的问题页面上报告任何错误,功能要求或改进建议:https://github.com/AxonFramework/AxonFramework/issues。所有想法都欢迎。报告错误时请尽可能准确。这将帮助我们重现问题,从而更快地解决问题。
- 如果您为您自己的应用程序创建了一个组件,而您认为这些组件可能会包含在框架中,请向我们发送包含源代码的修补程序或zip。 我们将对其进行评估并尝试将其纳入框架。 请确保使用javadoc正确记录代码。 这有助于我们理解正在发生的事情。
- 如果您知道您认为可以帮助我们的任何其他方式,请不要犹豫,向Axon Framework邮件列表发送消息。
架构概述
CQRS本身就是一个非常简单的模式。 它只规定处理命令的应用程序组件应与处理查询的组件分离。虽然这种分离本身非常简单,但是当与其他模式结合使用时,它提供了许多非常强大的功能。 Axon提供了构建模块,可以更轻松地实现可与CQRS结合使用的不同模式。下图显示了基于CQRS的事件驱动架构的扩展布局示例。 显示在左侧的UI组件以两种方式与应用程序的其余部分交互:它向应用程序发送命令(如上部分所示),并向应用程序查询信息(如下部分所示)。
命令通常由简单而直接的对象表示,这些对象包含执行命令处理程序所需的所有数据。一个命令用名字表达它的意图。对Java语言来说,这意味着使用类名来确定需要完成什么,并且命令的各个字段提供了执行此操作所需的信息。命令总线接收命令并将它们路由到命令处理程序。每个命令处理程序都会响应特定类型的命令,并根据命令的内容执行逻辑。在某些情况下,您也可以执行如验证,日志记录或授权逻辑。
命令处理程序从存储库中检索域对象(聚合)并执行它们的方法来更改它们的状态。这些聚合通常包含实际的业务逻辑,因此负责维护自己的状态。聚合的状态变化导致产生领域事件。领域事件和聚合形成领域模型。
存储库负责提供对聚合的访问。通常,这些存储库仅通过聚合的唯一标识符查找聚合。一些存储库将存储聚合本身的状态(例如使用对象关系映射),而另一些则在一个事件存储库中保存聚合经历的状态改变过程。存储库还负责在其后备存储中保留对聚合所做的更改。
在某些情况下,事件处理需要将新命令发送给应用程序。一个例子是收到订单时。这可能意味着客户的账户应该以购买金额记帐,并且必须通知运送准备运送所购买的商品。在许多应用中,逻辑将变得比这更复杂:如果客户没有及时付款,该怎么办?您会立即寄出货物,还是先等待付款? Saga是CQRS中的概念,用于管理这些复杂的业务事物。
自从Axon 3.1框架提供了处理查询的组件。 查询总线接收查询并将它们路由到查询处理程序。查询处理程序被注册在查询总线中,包括处理查询的类型以及查询响应的类型。
查询和结果类型通常都是简单的只读DTO对象。 这些DTO的内容通常由用户界面的需求驱动。在大多数情况下,它们直接映射到UI中的特定视图。
可以为同一类型的查询和响应类型注册多个查询处理程序。当分派查询时,客户可以指示他是想要一个结果还是来自所有可用的查询处理程序;
模块结构
Axon框架由一些针对CQRS特定问题领域的模块组成。根据项目的确切需要,您需要包含一个或多个这些模块。从Axon 2.1开始,所有模块都是OSGi兼容的软件包。
这意味着在清单文件中已经包含必需的头文件并声明它们导入和导出的包。目前,只需要Slf4J软件包(1.7.0 <版本<2.0.0)。 所有其他导入都标记为可选,尽管您很可能需要其他的导入。
主要模块
Axon的主要模块是经过全面测试的模块,并且足够健壮,可用于要求苛刻的生产环境。所有这些模块的maven groupId是org.axonframework。
正如其名称所示,核心模块包含了Axon的核心组件。如果您使用单节点安装程序,则该模块可能会提供您需要的所有组件。所有其他的Axon模块都依赖于这个模块,所以它必须始终在类路径中可用。
测试模块包含可用于测试基于Axon的组件的测试装置,例如您的命令处理程序,聚合和Sagas。您通常在运行时不需要此模块,只需在测试期间将其添加到类路径中即可。
分布式CommandBus模块包含可用于在多个节点上分发命令的实现。它带有用于连接这些节点的JGroups和Spring Cloud连接器。
AMQP模块提供的组件允许您使用基于AMQP的消息代理作为分发机制来构建EventBus。这允许有保证的传输,即使事件处理程序节点暂时不可用。
Spring模块允许在Spring应用程序上下文中配置Axon组件。 它还提供了许多特定于SpringFramework的构建块实现,例如用于在Spring消息通道上发布和检索Axon事件的适配器。
MongoDB是一个基于文档的NoSQL数据库。 Mongo模块提供Event和Saga Store实现,将事件流和传奇存储在MongoDB数据库中。
几个AxonFramework组件提供监视信息。 度量模块提供了基于Codehale的基本实现来收集监控信息。
使用Axon API
CQRS是一种架构模式,无法提供适合所有项目的单一解决方案。显然,Axon框架并不试图提供这种解决方案。取而代之的是,Axon提供了遵循最佳实践的实现方法,以及根据您的具体要求调整每个实现的方法。
几乎所有的基础设施构建块都会提供钩子点(例如拦截器,解析器等),允许您将特定于应用程序的行为添加到这些构建块。在很多情况下,Axon将为那些适合大多数用例的钩子点提供实现。如果需要,你可以简单地实现你自己的。
非基础结构对象(如消息)通常是内容不可变的。这确保了这些对象可以安全地在多线程环境中使用,而没有副作用。
为确保最大限度的定制,所有的Axon组件都使用接口进行定义。提供了抽象和具体的实现,以帮助你以你的方式定制。始终可以使用该接口构建任何构建块的完全自定义实现。
Spring 支持
Axon Framework为Spring提供了广泛的支持,但并不要求您使用Spring来使用Axon。所有组件都可以通过编程进行配置,并且不需要类路径中的Spring。 但是,如果你使用Spring,通过使用Spring的注解支持,许多配置变得更加容易
消息传递的概念
Axon的核心概念之一就是通讯。 组件之间的所有通信都使用消息对象完成。 这为这些组件提供了所需的位置透明度,以便在必要时扩展和分配这些组件。
尽管所有这些消息都实现了消息接口,但消息的不同类型和消息的处理方式之间有明显的区别。所有消息都包含有效负载,元数据和唯一标识符。
消息的有效载荷是消息的内容。元数据允许您描述消息发送的上下文。例如,您可以存储跟踪信息,以便跟踪消息的来源或原因。您还可以在元数据中存储信息来描述正在执行命令的安全上下文。
请注意,所有消息都是不可变的。将数据存储在消息中实际上意味着根据前一个消息创建新的消息,并添加额外的信息。这保证了消息可以安全地在多线程和分布式环境中使用。
命令
命令描述了改变应用程序状态的意图。它们被实现为(最好是只读的)POJO并使用CommandMessage的实现来进行封装。
命令总是只有一个目的地。尽管发件人并不关心哪个组件处理命令或组件驻留的位置,但对知道它的结果可能很有趣。这就是为什么通过命令总线发送的命令消息允许返回结果的原因。
事件
事件是描述应用程序中发生的事件的对象。事件的典型来源是聚合。在Aggregate内发生重要事件时,它将引发一个事件。 在Axon Framework中,事件可以是任何对象。 强烈建议您确保所有事件都是可序列化的。
当事件分派时,Axon将它们包装在一个EventMessage中。使用的消息的实际类型取决于事件的来源。当一个事件由一个Aggregate引发时,它被包装在一个DomainEventMessage(它扩展了EventMessage)中。 所有其他事件都包装在一个EventMessage中。 除了像唯一标识符这样的常见消息属性外,EventMessage还包含一个时间戳。DomainEventMessage还包含引发该事件的聚合的类型和标识符。 它还包含聚集事件流中事件的序列号,它允许重现事件的顺序。
注意,即使DomainEventMessage包含对AggregateIdentifier的引用,您也应始终在实际的Event中包含标识符。虽然EventEventMessage使用DomainEventMessage中的标识符来存储事件,但对于其它情况不会提供一个可靠的值作为标识符。
原始的Event对象存储为EventMessage的Payload。 在有效负载旁边,您可以将信息存储在事件消息的元数据中。元数据的目的是存储有关事件的附加信息,该事件主要不是作为业务信息。审计信息就是一个典型的例子。它允许您查看在哪些情况下引发了事件,例如触发处理的用户帐户或处理该事件的计算机的名称。
一般而言,您不应将业务的决策基于事件消息的元数据中的信息。如果是这样的话,你可能得到的是事件本身的附加信息而非业务内容信息。元数据通常用于报告,审计和跟踪。
虽然这不是强制的,但最好将所有字段设为final,并通过在构造函数中初始化,来让领域事件的内容不可变。 如果事件结构过于繁琐,请考虑使用Builder模式。
尽管领域事件在技术上表明了状态的变化,但您也应该尝试在事件中捕捉到状态的意图。一个好的做法是使用领域事件的抽象实现来捕获某个状态已经改变的事实,并使用该抽象类的具体子实现来指示改变的意图。例如,您可以拥有一个抽象的AddressChangedEvent,以及两个实现ContactMovedEvent和AddressCorrectedEvent的捕获状态更改的意图。 一些监听器不关心意图(例如数据库更新事件监听器)。这些将会听取抽象类型。其他监听器确实关心意图,他们会监听到具体的子类型(例如,向客户发送地址更改确认电子邮件)。
在事件总线上分派事件时,您需要将其包装在事件消息中。 GenericEventMessage是一个实现,它允许你将消息中的事件包装起来。 您可以使用构造函数或静态的asEventMessage()方法。 后者检查给定参数是否已经实现了消息接口。如果是这样,它要么直接返回(如果它实现了EventMessage),要么使用给定消息的有效载荷和元数据返回一个新的GenericEventMessage。 如果一个事件由一个聚合apply(或publish),Axon将会自动将该事件包装在一个DomainEventMessage中,该消息包含聚集的标识符,类型和序列号。
查询
查询描述了对信息或状态的请求。一个查询可以有多个处理程序。当分派查询时,客户端指示他是想要一个结果还是来自所有可用的查询处理程序
工作单元(Unit of Work)
工作单元是Axon框架中的一个重要概念,但在大多数情况下,您不可能直接与其交互。消息的处理被视为一个单元。工作单元的目的是协调处理消息(命令,事件或查询)期间执行的操作。组件可以注册要在工作单元的每个阶段执行的操作,例如onPrepareCommit或onCleanup。您不太可能需要直接访问工作单元。它主要由Axon提供的构建模块使用。如果您出于任何原因确定需要访问它,有几种方法可以获得它。处理程序通过handle方法中的参数接收工作单元。如果您使用注解,则可以将一个UnitOfWork类型的参数添加到注解的方法中。在其他位置,您可以通过调用CurrentUnitOfWork.get()来检索绑定到当前线程的工作单元。请注意,如果没有工作单元绑定到当前线程,则此方法将引发异常。使用CurrentUnitOfWork.isStarted()查找是否有可用的工作单元。
要求访问当前工作单元的一个原因是附加需要在消息处理过程中多次重复使用的资源,或者在工作单元完成时需要清理创建的资源。在这种情况下,unitOfWork.getOrComputeResource()和onRollback(),afterCommit()和onCleanup()等生命周期回调方法允许您对所需资源进行注册并声明在处理本工作单元期间要采取的操作。
请注意,工作单元仅仅是变化的缓冲区,而不是事务的替代。虽然所有分阶段的变更只是在工作单元提交时才提交,但其提交不是原子的。这意味着,如果提交失败,一些更改可能会继续保留,而其他更改则不会。最佳实践规定Command不应包含多个操作。 如果你坚持这种做法,一个工作单元将包含一个单一的操作,使它可以安全地使用。如果您在工作单元中有更多的操作,那么您可以考虑将事务附加到工作单元中进行提交,例如,使用unitOfWork.onCommit(..)来注册工作单元提交时需要采取的事务操作。
当处理消息时,您的处理程序可能会抛出异常。默认情况下,未经检查的异常将导致UnitOfWork回滚所有更改。 使得预期的副作用被取消。
Axon提供了一些开箱即用的回滚策略:
- RollbackConfigurationType.NEVER将始终提交工作单元
- RollbackConfigurationType.ANY_THROWABLE,将在发生异常时始终回滚
- RollbackConfigurationType.UNCHECKED_EXCEPTIONS将回滚到错误和运行时异常
- RollbackConfigurationType.RUNTIME_EXCEPTION将在运行时异常中回滚(但不包括错误)
使用Axon组件处理消息时,工作单元的生命周期将自动为您管理。 如果您选择不使用这些组件,而是实现自己的处理,则需要以编程方式启动并提交(或回滚)工作单元。在大多数情况下,DefaultUnitOfWork将为您提供所需的功能。 它期望处理发生在单个线程内。要在Unit Of Work中执行任务,只需在新的DefaultUnitOfWork上调用UnitOfWork.execute(Runnable)或UnitOfWork.executeWithResult(Callable)即可。 工作单元将在任务完成时启动并提交,或者在任务失败时回滚。如果您需要更多控制,您也可以选择手动启动,提交或回滚工作单元。
典型用法如下:
工作单位有几个阶段。每次进入另一阶段时,都会通知UnitOfWork侦听器:
UnitOfWork uow = DefaultUnitOfWork.startAndGet(message);
//接着,或者使用自动提交方式: uow.executeWithResult(() -> ... 这里写逻辑);
//或者手动提交或回滚:
try {
//这里写业务逻辑
uow.commit();
} catch (Exception e) {
uow.rollback(e);
//也可以重新抛出异常...
}
- 活动阶段:这是工作单元开始的地方。工作单元通常在此阶段通过CurrentUnitOfWork.set(UnitOfWork)注册到当前线程中。随后通常由消息处理程序处理消息。
- 提交阶段:在完成消息处理之后但在提交工作单元之前,会调用onPrepareCommit侦听器。 如果工作单元绑定到事务,则调用onCommit侦听器来提交任何支持事务,当提交成功时,调用afterCommit监听器。 如果提交或任何步骤失败,则会调用onRollback侦听器。 如果可用,消息处理程序结果包含在Unit Of Work的ExecutionResult中。
- 清理阶段:这是本工作单元拥有的任何资源(如锁)将被释放的阶段。如果多个工作单元嵌套,则清理阶段被推迟,直到外部工作单元准备清理。
消息处理过程可以被认为是一个原子程序; 它应该完全处理,或者根本不处理。 Axon Framework使用Unit Of Work来跟踪消息处理程序执行的操作。处理程序完成后,Axon将尝试提交在Unit Of Work中注册的操作。
可以将事务绑定到工作单元。许多组件(如CommandBus和QueryBus实现以及所有异步处理事件处理器)都允许您配置事务管理器。此事务管理器将用于创建事务,并将它绑定到管理消息处理的工作单元。
当应用程序组件需要得到消息处理的不同阶段(如数据库连接或EntityManager)中的资源时,可以将这些资源连接到工作单元。通过 unitOfWork.getResources()方法允许您访问附加到当前工作单元的资源。直接在工作单元上提供的这几种帮助方法,会使得处理资源更加轻松。
当嵌套的工作单元需要能够访问资源时,建议可以使用unitOfWork.root()得到根工作单元并将资源注册给它。
配置API
Axon在业务逻辑和基础架构配置方面保持严格的分离。 为了做到这一点,Axon将提供一些构建模块来处理基础设施问题,例如消息处理器周围的事务管理。 消息的实际有效负载的处理应该尽可能在与Axon无关的Java类中实现。
为了简化这些基础设施组件的配置并定义它们与每个功能组件的关系,Axon提供了一个配置API。
获取默认配置非常简单:
Configuration config =DefaultConfigurer.defaultConfiguration().buildConfiguration();
此配置提供构建块,用于消息分发,这些消息被分发消息所在的线程内的消息处理器进行处理。显然,这种配置不会很有用。您必须将您的命令模型对象和事件处理程序注册到此配置才有用。
为此,请使用.defaultConfiguration()方法返回的Configurer实例。
Configurer configurer =DefaultConfigurer.defaultConfiguration();
配置器提供了许多方法,允许您注册这些组件。如何配置这些内容将在每个组件的相应章节中详细介绍。
组件注册的一般形式如下:
Configurerconfigurer = DefaultConfigurer.defaultConfiguration();
configurer.registerCommandHandler(c -> doCreateComponent());
请注意registerCommandBus调用中的lambda表达式。此表达式的c参数是描述完整配置的配置对象。 如果您的组件需要其他组件正常运行,则可以使用此配置来获取它们。例如,要注册需要序列化程序的命令处理程序,请执行以下操作:
configurer.registerCommandHandler(c-> new MyCommandHandler(c.serializer());
并非所有组件都有其明确的访问方法。要从配置中获取组件,请使用:
该组件必须使用configurer.registerComponent(componentType,builderFunction)向Configurer注册。 构建器函数将接收Configuration对象作为输入参数。
configurer.registerCommandHandler(c-> newMyCommandHandler(c.getComponent(MyOtherComponent.class));
使用Spring设置配置
使用Spring时,不需要明确使用配置器。 相反,您可以简单地将@EnableAxon放在您的某个[email protected]类上。Axon将使用Spring应用程序上下文来定位构建块的特定实现,并为那些不在那里的构件提供默认值。 因此,在Spring中,您不必使用配置器来注册构建块,而只需将它们作为@Bean在应用程序上下文中使用即可。