使用明确的命令调度机制具有许多优点。 首先,有一个对象清楚地描述了客户端的意图。 通过记录命令,您可以存储意图数据和相关数据以备将来参考。 命令处理还可以很容易地通过Web服务将命令处理组件公开给远程客户端。测试也变得更容易,您可以通过列出大量事件和命令(请参阅测试)来定义测试脚本,方法是定义开始情况(给定),执行命令(何时)和预期结果(然后)。 最后一个主要优势是,在同步和异步以及本地和分布式命令处理之间切换非常容易;
这并不意味着使用显式命令对象进行命令分派是实现它的唯一方法。 Axon的目标不是规定特定的工作方式,而是支持您按自己的方式进行操作,同时提供最佳实践作为默认行为。 您仍然可以使用可以调用的服务层来执行命令,该方法只需要启动一个工作单元(请参阅工作单元),并在方法完成时对其执行提交或回滚。
接下来的部分概述了与使用Axon框架设置Command调度基础架构有关的任务。
命令网关
Command Gateway是Command调度机制的便捷接口。 虽然您不需要使用网关来分派命令,但通常这是最简单的选择。有两种使用Command Gateway的方法。 首先是使用Axon提供的CommandGateway接口和DefaultCommandGateway实现。 命令网关提供了许多方法,允许您发送命令并同步(具有超时)或异步等待结果。另一种选择也许是最灵活的:您可以使用CommandGatewayFactory将几乎任何接口转换为CommandGateway。这使您可以使用强类型定义应用程序的接口,并声明自己的(可检查)业务异常。 Axon会在运行时自动为该接口生成一个实现。
配置命令网关
您的自定义网关和Axon提供的自定义网关都必须配置为至少可以访问Command Bus。另外,Command Gateway可以配置RetryScheduler,CommandDispatchInterceptors和CommandCallbacks。
当命令执行失败时,RetryScheduler能够安排重试。IntervalRetryScheduler是一个实现,它将以设定的时间间隔重试给定的命令,直到成功或完成最大次数的重试。仅当由于RuntimeException导致命令失败时才会调用重试调度程序,checked异常被视为“业务异常”,并且永远不会触发重试。 RetryScheduler的典型用法是在分布式CommandBus上分派命令时。如果某个节点发生故障,则重试调度程序将导致将一个命令分派给能够处理该命令的下一个节点(请参阅分发命令总线)。
CommandDispatchInterceptor允许在将CommandMessage发送到CommandBus之前对其进行修改,与在CommandBus上配置的CommandDispatchInterceptor相比,只有在通过此网关发送消息时才会调用这些拦截器。
每次发送命令时将调用CommandCallback。 这允许通过此网关执行一些命令的通用行为,而不管此命令的类型如何。
创建定制的命令网关
Axon允许自定义接口用作命令网关,该接口中声明了每个方法的参数类型、返回类型和声明的异常,使用此网关不仅方便,而且允许您在需要的地方模拟接口,使测试变得容易得多。
以下就是影响CommandGateway行为的参数:
第一个参数作为期望要发送的实际命令对象。
使用@MetaDataValue注解的参数将使用标识符从元数据字段取值
类型MetaData的参数将与CommandMessage上的MetaData合并。 如果它们的key相同,较迟MetaData的参数定义将覆盖较早MetaData的参数定义。
CommandCallback类型的参数在处理命令后将调用onSuccess或onFailure。
最后两个参数的类型可以是long(或int)和TimeUnit,在这种情况下,该方法将允许的执行时间是这些参数所指定的,超时后作出反应取决于方法中声明的异常(请参见下文)。请注意,如果方法的其他属性同时在避免调用被阻塞,则永远不会发生超时。
方法的声明返回值也会影响其行为:
void返回类型将导致方法立即返回,除非方法上还有其他指示要等待,例如超时或声明的异常。
Future,CompletionStage和CompletableFuture的返回类型将使该方法立即返回。 您可以使用从方法返回的CompletableFuture实例访问命令处理程序的结果,并且方法中声明的异常和超时被忽略。
任何其他返回类型都会导致方法阻塞,直到结果可用。结果将转换为返回类型(如果类型不匹配,则会导致ClassCastException)
异常类型对方法的行为有以下影响:
如果命令处理程序(或拦截器)抛出一个未被声明的异常,它将被包装在一个CommandExecutionException中,这是一个RuntimeException。
发生超时时,默认行为是从方法返回null。这可以通过声明一个TimeoutException来改变。 如果声明了此异常,则会抛出TimeoutException。
当线程在等待结果时被中断时,默认行为是返回null。 在这种情况下,被中断的标志被设置在线程上。 通过在方法上声明一个InterruptedException,此行为将改为抛出该异常。 抛出异常时,中断标志被移除,与java规范一致。
其他运行时异常可以在方法中声明,但除了向API用户作出澄清之外不会有任何效果。
最后,可以使用注释影响方法的行为:
如参数部分中所指定的,参数上的@MetaDataValue注释将使用元数据值作为该参数的值,注释中的key值表示元数据字段,用于从元数据的字段中取值。
使用@Timeout注解的方法将需要在指定的等待时间内返回。 如果该方法已经声明了超时参数,则该注释将被忽略。
使用@Timeout注解的类将导致在该类中声明的所有方法至多等待指定的时间量,除非它们使用它们自己的@Timeout注释或指定超时参数进行注释。
public interface MyGateway {
// fire and forget
void sendCommand(MyPayloadType command);
// method that attaches meta data and will wait for a result for 10 seconds
@Timeout(value = 10, unit = TimeUnit.SECONDS)
ReturnValue sendCommandAndWaitForAResult(MyPayloadType command, @MetaDataValue("userId") String userId);
// alternative that throws exceptions on timeout
@Timeout(value = 20, unit = TimeUnit.SECONDS)
ReturnValue sendCommandAndWaitForAResult(MyPayloadType command) throws TimeoutException, InterruptedException;
// this method will also wait, caller decides how long
void sendCommandAndWait(MyPayloadType command, long timeout, TimeUnit unit) throws TimeoutException, InterruptedException;
}
// To configure a gateway:
CommandGatewayFactory factory = new CommandGatewayFactory(commandBus);
// note that the commandBus can be obtained from the `Configuration` object returned o n `configurer.initialize()`.
MyGateway myGateway = factory.createGateway(MyGateway.class);
命令总线
命令总线是将命令调度到它们各自的命令处理程序的机制。 每个命令总是发送到一个命令处理程序。 如果没有命令处理程序可用于处理分发的命令,则会引发NoHandlerForCommandException异常。 在相同的命令处理类型上配置多个命令处理程序将导致处理程序相互替换, 在这种情况下,只有最后一个命令处理程序生效
调度命令
CommandBus提供了两种方法来将命令分配给它们各自的处理程序:
dispatch(commandMessage,callback)和dispatch(commandMessage)。第一个参数是包含实际分派的命令的消息。可选的第二个参数需要一个回调,该命令允许在命令处理完成时通知调度组件。这个回调有两个方法:onSuccess()和onFailure(),分别在命令处理正常返回或者抛出异常时调用。
调用组件不能假设回调是在调度该命令的同一个线程中执行的。如果调用线程在继续之前需要等待结果,则可以使用FutureCallback。它是Future(在java.concurrent包中定义)和Axon的CommandCallback的一个组合。或者,考虑使用CommandGateway。
如果应用程序对命令的结果不感兴趣,则可以使用dispatch(commandMessage)方法。
SimpleCommandBus
顾名思义,SimpleCommandBus是最简单的实现。它可以直接处理调度它们的线程中的命令。处理完命令后,修改后的聚合被保存,生成的事件将在同一个线程中发布。在大多数情况下,例如Web应用程序,此实现将满足您的需求。 SimpleCommandBus是配置API中默认使用的实现。
与大多数CommandBus实现一样,SimpleCommandBus允许配置拦截器。在命令总线上分派命令时调用CommandDispatchInterceptor。在实际的命令处理程序方法之前调用CommandHandlerInterceptor,这允许您修改或阻止该命令。有关更多信息,请参阅命令拦截器。
由于所有命令处理都在同一个线程中完成,因此此实现仅限于JVM的边界。 这种实施的表现很好,但并不出色。 要跨越JVM边界,或充分利用CPU周期,请查看其他CommandBus实现。
AsynchronousCommandBus
顾名思义,AsynchronousCommandBus实现从调度它们的线程异步执行命令。 它使用Executor在不同的Thread上执行实际的处理逻辑。默认情况下,AsynchronousCommandBus使用无限制的缓存线程池。 这意味着一个线程在分派Command时被创建。 已完成处理命令的线程将重新用于新命令。 如果线程60秒后没有处理的命令,线程将被停止。
或者,一个Executor实例可以配置不同的线程策略。
请注意,在停止应用程序时应关闭AsynchronousCommandBus,以确保任何等待的线程已正确关闭。 要关闭它,请调用shutdown()方法。 这也将关闭任何已经提供的Executor实例——如果它实现了ExecutorService接口。
DisruptorCommandBus
SimpleCommandBus具有合理的性能特性,但是,SimpleCommandBus需要被锁定以防止多个线程同时访问相同聚合的事实会导致处理开销和锁的争用。
DisruptorCommandBus采用不同的方法来执行多线程处理。不是每个线程都处理相同的过程,而是每个线程负责处理一个过程中的一个片段。 DisruptorCommandBus使用Disruptor(http://lmax-exchange.github.io/disruptor/)(一个用于并发编程的小型框架),通过采用不同的多线程方法来获得更好的性能。它不是在调用线程中进行处理任务,而是将任务交给两组线程,每个线程都负责处理一部分任务。第一组线程将执行命令处理程序,更改聚合的状态。第二组将存储事件并将其发布到事件存储。
虽然DisruptorCommandBus轻松超过SimpleCommandBus4倍,但有一些限制:
要构造一个DisruptorCommandBus实例,您需要一个EventStore。 该组件在存储库和事件存储中进行了说明。或者,您可以提供DisruptorConfiguration实例,该实例允许您调整配置以优化特定环境的性能:
消息处理拦截器
消息处理程序拦截器可以在命令处理之前和之后执行操作。拦截器甚至可以完全禁止命令处理,例如出于安全原因。
拦截器必须实现MessageHandlerInterceptor接口。这个接口声明了一个方法handle,它有三个参数:命令消息,当前的UnitOfWork和一个InterceptorChain。InterceptorChain用于连续调度过程。
与调度拦截器不同,处理程序拦截器是在命令处理程序的上下文中调用的。这意味着他们可以基于正在处理的消息将关联数据附加到工作单元,然后将该相关数据附加到在该工作单元上下文中创建的消息中。
处理程序拦截器通常也用于管理处理命令的事务。为此,需要注册一个TransactionManagingInterceptor,它要被配置一个TransactionManager来启动和提交(或回滚)实际的事务
分布式命令总线
前面描述的CommandBus实现只允许在单个JVM中分派命令消息。 有时候,你需要不同JVM中的多个CommandBuses实例组合为一个Command Bus。 在一个JVM的Command Bus上调度的命令应该无缝地传送到另一个JVM中的Command Handler,同时返回结果。
这就是DistributedCommandBus的功能。与其他CommandBus实现不同,DistributedCommandBus根本不调用任何处理程序。 它所做的只是在不同JVM上的命令总线实现之间形成一个“桥梁”。 每个JVM上的每个DistributedCommandBus实例称为“段”。
由于分布式命令总线本身是AxonFramework Core模块的一部分,所以您可以在axon-distributed-commandbus-...模块中找到这些组件。 如果您使用Maven,请确保您已设置适当的依赖关系。 groupId和版本与Core模块的相同。
DistributedCommandBus依赖于两个组件:实现JVM之间通信协议的CommandBusConnector和为每个传入命令选择目标的CommandRouter。该路由器基于路由策略计算得到路由Key,来定义应该路由到分布式命令总线的哪个段。只要网段的数量和配置没有变化。具有相同路由Key的两个命令将始终路由到相同的网段,通常,目标聚合的标识符被用作路由Key。
Axon提供了两个RoutingStrategy实现:MetaDataRoutingStrategy,它使用命令消息中的元数据属性来查找路由Key;以及AnnotationRoutingStrategy,它使用命令消息有效载荷上的@TargetAggregateIdentifier注释来提取路由Key。显然,你也可以提供你自己的实现。
默认情况下,如果没有Key可以从命令消息中解析,则RoutingStrategy实现将引发异常。通过在MetaDataRoutingStrategy或AnnotationRoutingStrategy的构造函数中提供UnresolvedRoutingKeyPolicy,可以更改此行为。有三种可能的政策:
JGroupsConnector
JGroupsConnector使用JGroups作为基础发现和调度机制。 描述JGroups的功能集超过了本参考指南的范围,所以请参阅JGroups用户指南了解更多详细信息。
由于JGroups同时处理节点的发现和它们之间的通信,因此JGroupsConnector既充当CommandBusConnector也充当CommandRouter.
Tips:您可以在axon-distributed-commandbus-jgroups模块中找到DistributedCommandBus的JGroups特定组件。
JGroupsConnector有四个强制配置元素:
注意:使用缓存时,应在ConsistentHash更改时将其清除,以避免潜在的数据损坏(例如,当命令未指定@TargetAggregateVersion并且有新的聚合快速加入和离开JGroup时,此时被修改的聚合仍旧缓存在别处);
最后,JGroupsConnector需要实际连接,以便将消息发送到其他段。 为此,请调用connect()方法。
JChannel channel = new JChannel("path/to/channel/config.xml");
CommandBus localSegment = new SimpleCommandBus();
Serializer serializer = new XStreamSerializer();
JGroupsConnector connector = new JGroupsConnector(channel, "myCommandBus", localSegment, serializer);
DistributedCommandBus commandBus = new DistributedCommandBus(connector, connector);
// on one node:
commandBus.subscribe(CommandType.class.getName(),handler);
connector.connect();
// on another node, with more CPU:
commandBus.subscribe(CommandType.class.getName(), handler);
commandBus.subscribe(AnotherCommandType.class.getName(), handler2); commandBus.updateLoadFactor(150); // defaults to 100
connector.connect();//from now on, just deal with commandBus as if it is local..
请注意,并不要求所有段都具有用于相同类型命令的命令处理程序。 您可以为不同的命令类型使用不同的段。 分布式命令总线将始终选择一个支持特定类型命令的处理节点来分派命令。
如果你使用Spring,你可能要考虑使用JGroupsConnectorFactoryBean。 它在ApplicationContext启动时自动连接Connector,并在ApplicationContext关闭时进行适当的断开连接。 此外,它对测试环境使用合理的默认值(但不应将其视为生产就绪),并对配置进行自动装配。
Spring Cloud Connector
Spring Cloud Connector安装使用SpringCloud描述的服务注册和发现机制来分发命令总线。 因此,您可以自由选择使用哪种Spring Cloud实现来分发您的命令。 示例实现是Eureka Discovery / Eureka服务器组合。
注意SpringCloudCommandRouter使用特定于SpringCloud的特定ServiceInstance.Metadata字段向系统中的所有节点通知其消息路由信息。 因此选择Spring Cloud的实现必须支持ServiceInstance.Metadata字段的使用,这点非常重要。 如果所需的SpringCloud实现不支持修改ServiceInstance.Metadata(例如Consul),则SpringCloudHttpBackupCommandRouter是一个可行的解决方案。 有关SpringCloudHttpBackupCommandRouter的配置细节,请参阅本章末尾的内容。
对每个SpringCloud实现进行描述将偏离本参考指南的主题。 因此,我们只引用他们各自的文件获取更多信息。
Spring Cloud Connector安装程序是SpringCloudCommandRouter和SpringHttpCommandBusConnector的组合,它们分别填充CommandRouter和CommandBusConnector的位置以用于DistributedCommandBus;
SpringCloudCommandRouter必须通过提供以下内容来创建:
SpringCloudCommandRouter的其他可选参数是:
SpringHttpCommandBusConnector需要三个参数来创建:
DistributedCommandBus的SpringCloud Connector特定组件可以在axon-distributed-commandbus-springcloud模块中找到
SpringCloudCommandRouter和SpringHttpCommandBusConnector都应该用于创建DistributedCommandsBus。 在SpringJava配置中,它看起来如下所示:
// Simple Spring Boot App providing the `DiscoveryClient` bean
@EnableDiscoveryClient
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
// Example function providing a Spring Cloud Connector @Bean
public CommandRouter springCloudCommandRouter(DiscoveryClient discoveryClient) {
return new SpringCloudCommandRouter(discoveryClient, new AnnotationRoutingStrategy());
}
@Bean
public CommandBusConnector springHttpCommandBusConnector(@Qualifier("localSegment")CommandBus localSegment,RestOperations restOperations,Serializer serializer) {
return new SpringHttpCommandBusConnector(localSegment, restOperations, serializer);
}
@Primary // to make sure this CommandBus implementation is used for autowiring
@Bean
public DistributedCommandBus springCloudDistributedCommandBus(CommandRouter comman dRouter,CommandBusConnector commandBusConnector) {
return new DistributedCommandBus(commandRouter, commandBusConnector);
}
}
// if you don't use Spring Boot Autoconfiguration, you will need to explicitly define the local segment:
@Bean
@Qualifier("localSegment")
public CommandBus localSegment() {
return new SimpleCommandBus();
}
注意:并不要求所有段都具有用于相同类型命令的命令处理程序。 您可以为不同的命令类型使用不同的段。 分布式命令总线将始终选择一个支持该特定类型命令的节点来分派命令。
Spring Cloud Http Back UpCommand Router
在内部,SpringCloudCommandRouter使用SpringCloud的ServiceInstance中包含的Metadata映射,在整个分布式Axon环境中传递允许的消息路由信息。如果所需的Spring Cloud实现不允许修改ServiceInstance.Metadata字段(例如Consul),则可以选择实例化SpringCloudHttpBackupCommandRouter而不是SpringCloudCommandRouter。
顾名思义,SpringCloudHttpBackupCommandRouter是在ServiceInstance.Metadata字段不包含预期的消息路由信息时,提供备份机制,该机制提供一个 HTTP 端点, 从中可以检索消息路由信息,并同时添加功能以查询群集中其他已知节点的端点,然后检索其消息路由信息。因此,备份机制功能是一个Spring Controller,用于在可指定端点接收请求,并使用RestTemplate向可指定端点上的其他节点发送请求。
要使用SpringCloudHttpBackupCommandRouter而不是SpringCloudCommandRouter,添加以下SpringJava配置(它取代了我们前面例子中的SpringCloudCommandRouter方法):
public class MyApplicationConfiguration {
@Bean
public CommandRouter springCloudHttpBackupCommandRouter(
DiscoveryClient discoveryClient,RestTemplate restTemplate,
@Value("${axon.distributed.spring-cloud.fallback-url}") String messageRoutingInformationEndpoint) {
return new SpringCloudHttpBackupCommandRouter(discoveryClient, new AnnotationR outingStrategy(), restTemplate, messageRoutingInformationEndpoint);
}
}