Axon(三)

三、消息的概念

3.1消息传递

消息传递是Axon的核心概念之一。组件之间的所有通信都是使用消息对象完成的。这使这些组件具有位置透明性,以便在必要时能够缩放和分发这些组件。‌

尽管所有这些消息都实现了接口,但是在不同类型的消息及其处理方式之间存在明显的区别。

所有消息都包含有效负载,元数据和唯一标识符。消息的有效负载是消息含义的功能描述。该对象的类名及其携带的数据的组合描述了应用程序对消息的含义。元数据可让您描述发送消息的上下文。例如,您可以存储跟踪信息,以允许跟踪消息的来源或原因。在命令被执行时,您还可以存储信息以描述这个命令的安全上下文。

Axon编码教程#2: -核心API

注意

请注意,所有消息都是不可变的。在消息中存储数据实际上意味着在前一条消息的基础上创建一条新消息,并添加了更多信息。这保证了在多线程和分布式环境中可以安全地使用消息。

命令

命令描述了更改应用程序状态的意图。它们被实现为(最好是只读的)pojo,使用CommandMessage的实现之一包装这些pojo。

命令总是只有一个目的地。虽然发送方不关心哪个组件处理命令或该组件驻留在何处,但了解它的结果可能很有趣。这就是为什么通过命令总线发送的CommandMessage允许返回结果。

日志

事件是描述应用程序中发生的事情的对象。典型的事件来源是聚合。当聚合中发生重要事件时,它将引发一个事件。在Axon Framework中,事件可以是任何对象。强烈建议您确保所有事件都可序列化。‌

调度事件时,Axon将它们封装在EventMessage中。实际使用的消息类型取决于事件的来源。当聚合引发事件时,它被包装在DomainEventMessage中(它扩展了EventMessage)。所有其他事件都封装在EventMessage中。除了常见的消息属性(如唯一标识符)外,EventMessage还包含时间戳。DomainEventMessage还包含引发事件的聚合的类型和标识符。它还包含聚合的事件流中事件的序列号,这允许复制事件的顺序。

注意

即使域事件信息包含对聚合标识符的引用,也应始终将标识符包含在实际事件本身中。域事件信息中的标识符由事件存储器用于存储事件,它并不一定总是为其他目的提供可靠的值

原始的事件对象存储为EventMessage的有效负载。在有效负载旁边,您可以将信息存储在EventMessage的元数据中。元数据的目的是存储有关事件的其他信息,这些信息主要不旨在用作业务信息。审计信息就是一个典型的例子。它使您可以查看在什么情况下引发了事件。例如触发处理的用户帐户,或处理事件的计算机的名称。

注意

通常,您不应基于事件消息的元数据中的信息来进行业务决策。如果真是这样,您可能已经附加了实际上应该属于事件本身的信息。元数据通常用于报告,审核和跟踪。

尽管没有强制执行,但最好的做法是使域事件不可变,最好是使所有字段都为final,并在构造函数中初始化事件。如果事件构造太麻烦,请考虑使用Builder模式。

注意

尽管域事件从技术上指示状态更改,但您也应该尝试捕获事件中状态的意图。一个好的实践是使用域事件的抽象实现来捕获特定状态已更改的事实,并使用该抽象类的具体子实现来指示更改的意图。例如,您可能有一个抽象类(AddressChangedEvent)和两个实现(ContactMovedEvent和AddressCorrectedEvent),它们捕获了状态更改的意图。一些监听器不在乎意图(例如,数据库更新事件监听器)。这些将监听抽象类型。其他监听器确实关心此意图,这些监听器将侦听具体的子类型(例如,向客户发送地址更改确认电子邮件)。

在事件总线上调度事件时,需要将事件包装在事件消息中。GenericEventMessage是一个实现,允许您将事件包装在消息中。可以使用构造函数或静态asEventMessage()方法。后者检查给定参数是否尚未实现消息接口。如果是这样,它要么直接返回(如果它实现EventMessage,),要么使用给定消息的有效负载和元数据返回一个新的GenericEventMessage。如果某个事件被聚合应用(发布),Axon将自动将该事件包装在包含聚合标识符、类型和序列号的DomainEventMessage中。

查询

查询描述了对信息或状态的请求。一个查询可以有多个处理程序。调度查询时,客户端会指示他是从一个查询还是从所有可用查询处理程序中获取结果。

3.2 对消息的剖析

在Axon中,组件之间的所有通信都是通过显式消息完成的,这些消息由消息接口表示。消息由有效载荷(表示实际功能消息的特定于应用程序的对象)和元数据(元数据是描述消息上下文的键值对)组成。             

消息的每个子接口表示一种特定类型的消息,并定义描述该消息的附加信息。与元数据不同,此附加信息定义了正确处理该类型消息所需的信息。              消息是不可变的。这意味着,要添加元数据元素,可以有效地创建一个新的消息实例,其中包含一个额外的(或其他)元数据元素。为了仍然能够将消息的两个Java实例视为表示同一个概念性消息,每个消息都有一个标识符。更改消息的元数据不会更改此标识符。

元数据

消息的元数据通常描述生成消息的上下文。例如,元数据可能包含有关导致该消息生成的消息的信息(如命令处理程序根据传入的命令生成事件)。

在Axon中,元数据表示为字符串到对象的映射。尽管您可以随意添加任何类型的对象作为“元数据”值,但我们强烈建议您坚持使用基本体和字符串(或最多是简单的对象)。在进行结构更改时,元数据具有与有效负载不同的灵活性。

与常规的Map<String,Object>不同,元数据在Axon中是不可变的。变异方法将创建并返回一个新实例,而不是修改现有实例。

MetaData metaData = MetaData.with("myKey", 42) // 1

                            .and("otherKey", "some value"); // 2

//第一行用指定的键值对创建实例,第二行添加额外的键值对,返回的新实例。

元数据在消息中的行为方式是相似的:

EventMessage eventMessage = 
        GenericEventMessage.asEventMessage("myPayload") // 1
                           .withMetaData(singletonMap("myKey", 42)) // 2
                          .andMetaData(singletonMap("otherKey", "some value")); // 3
/*
第一行创建一个以“myPayload”作为有效负载的EventMessage              
第二行的withMetaData用给定的映射替换消息中的任何元数据,在这种情况下java.util.Collections.singletonMap()用于定义单个条目。              
第三行andMetaData将给定映射中的条目添加到消息的元数据中。具有相等键的现有条目将被覆盖。
*/

注意:元数据还实现了Map<String,Object>,这意味着除了传递单个映射(或任何其他类型的Map),还可以使用MetaData.with(键,值)。但由于元数据不可变,所有的操作都会抛出UnsupportedOperationException。

特定消息的数据

某些类型的消息(message)提供额外的信息(在消息提供的信息之上)。例如,EventMessage(接口扩展消息)还提供时间戳,表示事件生成的时间。除了有效负载和元数据之外,QueryMessage还包含请求组件所期望的响应类型的描述。

3.3消息相关

在消息传递系统中,通常将消息分组或关联起来。在Axon框架中,命令消息可能导致一个或多个事件消息,而查询消息可能会导致一个或多个QueryResponse消息。通常,关联是使用特定的消息属性(所谓的关联标识符)来实现的。

3.3.1 相关消息提供者

Axon框架中的消息使用元数据属性来传输有关消息的元信息。元数据对象的类型为Map<String,object>,并与消息一起传递。为了填充在工作单元内生成的新消息的元数据,可以使用所谓的CorrelationDataProvider。它是负责基于此CorrelationDataProvider填充新消息的元数据的工作单元。Axon Framework目前提供了此功能接口的一些实现方式:

    • 消息源提供程序:默认情况下,MessageOriginProvider注册为要使用的相关数据提供程序。它负责将两个值从一个消息传递到另一个消息,即correlationId和traceId。消息的correlationId总是引用它源于的消息的标识符(即父消息)。另一方面,traceId引用启动消息链(即根消息)的消息标识符。创建新消息时,如果父消息中不存在这两个字段,则消息标识符将用于这两个字段。在一个示例中,如果您要处理一个命令消息,而该命令消息又发布了一个事件消息,则事件消息的元数据将基于以下内容填充:            

·correlationId的命令消息标识符。             

·命令消息在元数据中是否存在traceId,或者消息的traceId标识符。

    • SimpleCorrelationDataProvider              SimpleCorrelationDataProvider配置为无条件地将指定键的值从一条消息复制到另一条消息的元数据。必须使用要复制的提供程序的SimpleDataRelativeList调用该构造函数。下面是一个示例,讲述了如何配置它以复制myId和myId2值。
public class Configuration {
    public CorrelationDataProvider customCorrelationDataProvider() {
        return new SimpleCorrelationDataProvider("myId", "myId2");
    }
}
    • MultiCorrelationDataProvider能够组合多个相关数据提供者的效果。为此,必须使用提供程序列表调用MultiCorrelationDataProvider的构造函数,如下例所示:
public class Configuration {public CorrelationDataProvider customCorrelationDataProviders() {
        return new MultiCorrelationDataProvider<CommandMessage<?>>(
            Arrays.asList(
                new SimpleCorrelationDataProvider("someKey"),
                new MessageOriginProvider()
            )
        );
    }
}
  • 实现自定义相关数据提供程序             

如果预定义的提供程序不能满足您的需求,您可以始终实现自己的CorrelationDataProvider。该类必须实现CorrelationDataProvider接口,如下例所示:

public class AuthCorrelationDataProvider implements CorrelationDataProvider {private final Function<String, String> usernameProvider;public AuthCorrelationDataProvider(Function<String, String> userProvider) {
        this.usernameProvider = userProvider;
    }@Override
    public Map<String, ?> correlationDataFor(Message<?> message) {
        Map<String, Object> correlationData = new HashMap<>();
        if (message instanceof CommandMessage<?>) {
            if (message.getMetaData().containsKey("authorization")) {
                String token = (String) message.getMetaData().get("authorization");
                correlationData.put("username", usernameProvider.apply(token));
            }
        }
        return correlationData;
    }
}

3.3.2配置

当默认的MessageOriginProvider不足以满足您的用例时,需要在应用程序中注册(自定义)相关数据提供程序。如果您使用的是Axon配置API,请确保调用Configuration#configureCorrelationDataProviders方法来注册相关的数据提供程序。如果您依赖于springboot自动配置,只需提供一个公开springbean的工厂方法,如果您都需要,则提供多个方法。以下片段显示了一些可能的注册方法:

①Axon配置API

public class Configuration {
	public void configuring() {
		Configurer configurer = DefaultConfigurer.defaultConfiguration().configureCorrelationDataProviders(
			config -> Arrays.asList(
				new SimpleCorrelationDataProvider("someKey"),newMessageOriginProvider()
			)
		);
	}
}
  • Spring boot配置类
@Configuration
public class CorrelationDataProviderConfiguration {

	// 配置单个CorrelationDataProvider将自动覆盖默认的MessageOriginProvider
	@Bean
	public CorrelationDataProvider someKeyCorrelationProvider() {
		return new SimpleCorrelationDataProvider("someKey");
	}
	
	@Bean
	public CorrelationDataProvider messageOriginProvider() {
		return new MessageOriginProvider();
	}
}

3.4消息拦截

拦截器有两种不同类型:调度拦截器和处理程序拦截器。在将消息调度到消息处理程序之前,将调用调度拦截器。在那时,甚至可能还不知道该消息存在处理程序。处理程序拦截器在消息处理程序被调用之前被调用。

命令拦截器

使用命令总线的优点之一是能够根据所有传入命令进行操作。示例是日志记录或身份验证,无论命令类型如何,都可能要执行这些操作。这是使用拦截器完成的。

命令调度拦截器

在命令总线上分派命令时,将调用消息调度拦截器。他们可以通过添加元数据来更改命令消息。他们还可以通过引发异常来阻止命令。这些拦截器总是在分派命令的线程上调用。

让我们创建一个MessageDispatchInterceptor消息调度拦截器,它分派的每个命令都消息记录在CommandBus上。

public class MyCommandDispatchInterceptor implements MessageDispatchInterceptor<CommandMessage<?>> {

    private static final Logger LOGGER = LoggerFactory.getLogger(MyCommandDispatchInterceptor.class);

    @Override
    public BiFunction<Integer, CommandMessage<?>, CommandMessage<?>> handle(List<? extends CommandMessage<?>> messages) {
        return (index, command) -> {
            LOGGER.info("Dispatching a command {}.", command);
            return command;
        };
    }
}

通过执行以下操作,我们可以使用CommandBus来注册此调度拦截器:

public class CommandBusConfiguration {

    public CommandBus configureCommandBus() {
        CommandBus commandBus = SimpleCommandBus.builder().build();
        commandBus.registerDispatchInterceptor(new MyCommandDispatchInterceptor());
        return commandBus;
    }
}

结构验证

命令没有以正确的格式包含所有必需的信息,则没有必要处理该命令。实际上,应该尽早阻止缺少信息的命令,最好在开始事务处理之前就将其阻塞。因此,拦截器应检查所有传入的命令以获取此类信息。这称为结构验证。

Axon Framework支持基于JSR 303 Bean验证的验证。这样,您便可以使用诸如@NotEmpty@Pattern的注释来注释命令中的字段。您需要在类路径中包含JSR 303实现(例如Hibernate-Validator)。然后,在命令总线上配置一个BeanValidationInterceptor,它将自动查找并配置您的验证程序实现。您可以根据自己的特定需求对其进行微调也可以用默认设置,使它使用合理的值。

拦截器设定技巧

您希望在无效命令上花费尽可能少的资源。因此,这个拦截器通常被放置在拦截器链的正前方。在某些情况下,可能需要先放置LoggingInterceptor或AuditingInterceptor,然后紧接着是validating interceptor。

BeanValidationInterceptor还实现了MessageHandlerInterceptor,允许您将其配置为处理程序监听器。

命令处理程序拦截器

消息处理程序拦截器可以在命令处理之前和之后执行操作。拦截器甚至可以完全阻止命令处理,例如出于安全原因。                    

拦截器必须实现MessageHandlerInterceptor接口。这个接口声明了一个方法handle,它接受两个参数:当前UnitOfWork和一个拦截器链。拦截器链用于继续调度过程。UnitOfWork提供(1)正在处理的消息,(2)提供在(命令)消息处理之前、期间或之后连接逻辑的可能性(有关阶段的更多信息,请参阅工作单元)。             

拦截器与上下文处理程序不同,拦截器是在命令调度中调用的。例如,这意味着它们可以根据正在处理的消息将相关数据附加到工作单元。然后,该关联数据将附加到在该工作单元上下文中创建的消息。             

处理程序拦截器通常也用于管理围绕命令处理的事务。为此,请注册一个TransactionManagingInterceptor,该拦截器将配置为使TransactionManager来启动和提交(或回滚)实际事务。             

让我们创建一个消息处理程序拦截器,它只允许处理将axonUser作为元数据中userId字段值的命令。如果用户标识不在元数据中,则将引发一个异常,该异常将阻止处理该命令。另外,如果userId的值与axonUser不匹配,我们也不会继续向上走。与调度拦截器不同,处理程序拦截器在命令处理程序的上下文中调用。例如,这意味着他们可以根据正在处理的消息将相关性数据附加到工作单元。然后,此关联数据将附加到在该工作单元的上下文中创建的消息上。

处理程序拦截器通常还用于管理命令处理周围的事务。为此,请注册一个TransactionManagingInterceptor,然后配置一个TransactionManager,以启动和提交(或回滚)实际事务。

让我们创建一个消息处理程序拦截器,它只允许处理将axonUser作为元数据中userId字段值的命令。如果用户标识不在元数据中,则将引发一个异常,该异常将阻止处理该命令。另外,如果userId的值与axonUser不匹配,我们也不会继续执行程序。

public class MyCommandHandlerInterceptor implements MessageHandlerInterceptor<CommandMessage<?>> {

    @Override
    public Object handle(UnitOfWork<? extends CommandMessage<?>> unitOfWork, InterceptorChain interceptorChain) throws Exception {
        CommandMessage<?> command = unitOfWork.getMessage();
        String userId = Optional.ofNullable(command.getMetaData().get("userId"))
                                .map(uId -> (String) uId)
                                .orElseThrow(IllegalCommandException::new);
        if ("axonUser".equals(userId)) {
            return interceptorChain.proceed();
        }
        return null;
    }
}

我们可以使用CommandBus注册处理程序拦截器:

public class CommandBusConfiguration {

    public CommandBus configureCommandBus() {
        CommandBus commandBus = SimpleCommandBus.builder().build();
        commandBus.registerHandlerInterceptor(new MyCommandHandlerInterceptor());
        return commandBus;
    }

}

@CommandHandlerInterceptor 注解

该框架能够将处理程序拦截器作为@CommandHandlerInterceptor注解添加到聚合/实体上。聚合上的方法和“常规”命令处理程序拦截器的区别在于,使用注解,您可以根据给定聚合的当前状态做出决策。带注解的命令处理程序拦截器的一些属性是:             

    • 注释可以放在聚合中的实体上。             
    • 当命令处理程序位于子实体中时,可以在聚合根级别截获命令。             
    • 可以通过从带注释的命令处理程序拦截器触发异常来阻止命令执行。             
    • 可以将拦截器链定义为命令处理程序拦截器方法的参数,并使用它来控制命令执行。             
    • 通过使用@CommandHandlerInterceptor注释的commandNamePattern属性,我们可以截获与所提供正则表达式匹配的所有命令。             
    • 事件可以从带注释的命令处理程序拦截器应用。     

在下面的示例中,我们可以看到一个带@CommandHandlerInterceptor注释的方法,如果命令的状态字段与聚合的状态字段不匹配,则可以阻止命令执行:

public class GiftCard {
    //..
    private String state;
    //..
    @CommandHandlerInterceptor
    public void intercept(RedeemCardCommand command, InterceptorChain interceptorChain) {
        if (this.state.equals(command.getState())) {
            interceptorChain.proceed();
        }
    }
}

请注意,它本质上是@CommandHandlerInterceptor@MessageHandlerInterceptor此处描述的更具体的实现。

事件拦截器

与命令消息类似,事件消息也可以在发布和处理之前被拦截,以对所有事件执行其他操作。这归结为两种类型的消息拦截器:调度拦截器和处理程序拦截器。

事件调度拦截器

发布事件时,将调用注册到事件总线的任何消息调度拦截器。他们可以通过添加元数据来更改事件消息。它们还可以为您提供事件发布的总体日志记录功能。这些拦截器总是在发布事件的线程上调用。

让我们创建一个事件消息调度拦截器,它记录正​​在EventBus上发布的每个事件消息。

public class EventLoggingDispatchInterceptor
                implements MessageDispatchInterceptor<EventMessage<?>> {

    private static final Logger logger =
                LoggerFactory.getLogger(EventLoggingDispatchInterceptor.class);

    @Override
    public BiFunction<Integer, EventMessage<?>, EventMessage<?>> handle(
                List<? extends EventMessage<?>> messages) {
        return (index, event) -> {
            logger.info("Publishing event: [{}].", event);
            return event;
        };
    }
}

然后,我们可以通过执行以下操作,向EventBus分配此调度拦截器:

public class EventBusConfiguration {

    public EventBus configureEventBus(EventStorageEngine eventStorageEngine) {
        // note that an EventStore is a more specific implementation of an EventBus
        EventBus eventBus = EmbeddedEventStore.builder()
                                              .storageEngine(eventStorageEngine)
                                              .build();
        eventBus.registerDispatchInterceptor(new EventLoggingDispatchInterceptor());
        return eventBus;
    }
}

事件处理程序拦截器

消息处理程序拦截器可以在事件处理之前和之后执行操作。例如,出于安全原因,拦截器甚至可以完全阻止事件处理。             

拦截器必须实现MessageHandlerInterceptor接口。拦截器声明了当前接口的一个handle()方法和两个参数:拦截链和当前UnitOfWork。拦截器链用于继续调度过程。UnitOfWork提供(1)正在处理的消息,(2)提供在(事件)消息处理之前、期间或之后连接逻辑的可能性(有关阶段的更多信息,请参阅工作单元)。              

与分派拦截器不同,处理程序拦截器是在事件处理程序的上下文中调用的。例如,这意味着它们可以根据正在处理的消息将相关数据附加到工作单元。然后,该关联数据将附加到在该工作单元上下文中创建的事件消息(event message)。             

让我们创建一个消息处理程序拦截器,它只允许处理包含axonuser作为元数据中userId字段值的事件。如果元数据中不存在userId,则将引发一个异常,该异常将阻止事件的处理。如果userId的值与axonUser不匹配,我们也不会继续向上走。对事件消息进行身份验证(如本例所示)是MessageHandlerInterceptor的常规用例。

public class MyEventHandlerInterceptor
        implements MessageHandlerInterceptor<EventMessage<?>> {

    @Override
    public Object handle(UnitOfWork<? extends EventMessage<?>> unitOfWork,
                         InterceptorChain interceptorChain) throws Exception {
        EventMessage<?> event = unitOfWork.getMessage();
        String userId = Optional.ofNullable(event.getMetaData().get("userId"))
                                .map(uId -> (String) uId)
                                .orElseThrow(IllegalEventException::new);
        if ("axonUser".equals(userId)) {
            return interceptorChain.proceed();
        }
        return null;
    }
}

我们可以使用EventProcessor注册处理程序拦截器,:

public class EventProcessorConfiguration {

    public void configureEventProcessing(Configurer configurer) {
        configurer.eventProcessing()
                  .registerTrackingEventProcessor("my-tracking-processor")
                  .registerHandlerInterceptor("my-tracking-processor",
                                              configuration -> new MyEventHandlerInterceptor());
    }
}

拦截器注册

CommandBusQueryBus都可以有处理程序拦截器和调度拦截器,而EventBus与以上两者不同,它只能注册调度拦截器。这是因为EventBus的唯一目的是事件发布/调度,因此它们是注册事件调度拦截器的地方。EventProcessors负责处理事件消息,因此它们是注册事件处理程序拦截器的地方。

查询拦截器

使用查询总线的优点之一是能够根据所有传入查询执行操作。示例是日志记录或身份验证,无论查询类型如何,都要执行它。这是使用拦截器完成的。

查询调度拦截器

当在查询总线上调度查询或在查询更新发射器上调度对查询消息的订阅更新时,将调用消息调度拦截器。他们可以通过添加元数据来更改消息。他们还可以通过引发异常来阻止处理程序执行。这些拦截器总是在调度消息的线程上调用。

结构验证

如果查询不包含正确格式的所有必需信息,则没有必要处理查询。实际上,应该尽早阻止缺少必要信息的查询。因此,拦截器应检查所有传入查询中此类信息的可用性。这称为结构验证。

Axon Framework支持基于JSR 303 Bean验证的验证。这样,您便可以使用诸如@NotEmpty@Pattern的注释来注释查询中的字段。您需要在类路径中包含JSR 303实现(例如Hibernate-Validator)。然后,在查询总线上配置 BeanValidationInterceptor ,它将自动查找并配置您的验证程序实现。当它使用合理的默认值时,您可以根据自己的特定需求对其进行微调。

拦截器定制技巧

您想要在无效查询上花费尽可能少的资源。因此,此拦截器通常位于拦截器链的最前面。在某些情况下,可能需要首先放置日志记录或审核拦截器,然后紧跟验证拦截器。

BeanValidationInterceptor同时实现了MessageHandlerInterceptor

,这使得你可以像配置处理程序拦截器一样配置它。

查询处理程序拦截器

消息处理程序拦截器(message handler interceptor)可以在查询处理之前和之后采取措施。出于安全原因,拦截器甚至可以完全阻止查询处理。

拦截器必须实现MessageHandlerInterceptor接口。此接口声明一个handle()方法,该方法带有两个参数:当前UnitOfWork和一个拦截器链 。拦截器链InterceptorChain将用于继续调度过程。UnitOfWork可为您提供(1)正在处理的消息,以及(2)提供在(查询)消息处理之前,期间或之后绑定逻辑的可能性(有关阶段的更多信息,请参见工作单元)。

与程序调度拦截器不同,在查询处理程序的上下文中调用处理程序拦截器。例如,这意味着他们可以根据正在处理的消息将相关性数据附加到工作单元。然后,此关联数据将附加到在该工作单元的上下文中创建的消息上。

@MessageHandlerInterceptor

除了在处理消息的组件上定义整体MessageHandlerInterceptor实例(例如命令,查询或事件)外,还可以为包含处理程序的特定组件定义处理程序拦截器。这可以通过添加处理消息的方法并结合@MessageHandlerInterceptor注释来实现。添加这种方法可以使您更精细地控制哪些消息处理组件应该做出反应以及它们如何做出反应。

添加@MessageHandlerInterceptor时,会为您提供几个句柄,例如:

  • MessageHandlerInterceptor实例与拦截器链一起工作,以决定何时处理链中的其他拦截器。拦截器链是一个可选参数,可以将其添加到拦截方法中以提供相同的控制。如果没有此参数,一旦方法退出,框架将调用InterceptorChain#proceed。
  • 您可以定义应处理Message的拦截器的类型。默认情况下,它对任何Message都做出反应。如果需要特定EventMessage的拦截器,则注释上的messageType参数应设置为EventMessage.class
  • 为了更细粒度地控制哪些消息应该对拦截器作出反应,可以指定要处理的消息中包含的payloadType。

以下代码片段显示了使用@MessageHandlerInterceptor注释的一些可能方法:

//简单的@MessageHandlerInterceptor方法

public class CardSummaryProjection {
    /*
     * Some @EventHandler and @QueryHandler annotated methods
     */
    @MessageHandlerInterceptor
    public void intercept(Message<?> message) {
        // Add your intercepting logic here based on the
    }
}
//定义消息类型的@MessageHandlerInterceptor方法
 
 
public class CardSummaryProjection {
    /*
     * Some @EventHandler and @QueryHandler annotated methods
     */
    @MessageHandlerInterceptor(messageType = EventMessage.class)
    public void intercept(EventMessage<?> eventMessage) {
        // Add your intercepting logic here based on the
    }
}
public class CardSummaryProjection {
    /*
     * Some @EventHandler and @QueryHandler annotated methods
     */
    @MessageHandlerInterceptor(
        messageType = EventMessage.class,
        payloadType = CardRedeemedEvent.class
    )
    public void intercept(CardRedeemedEvent event) {
        // Add your intercepting logic here based on the
    }
}
//定义InterceptorChain参数的@MessageHandlerInterceptor方法
 
 
public class CardSummaryProjection {
    /*
     * Some @EventHandler and @QueryHandler annotated methods
     */
    @MessageHandlerInterceptor(messageType = QueryMessage.class)
    public void intercept(QueryMessage<?, ?> queryMessage,
                          InterceptorChain interceptorChain) throws Exception {
        // Add your intercepting logic before
        interceptorChain.proceed();
        // or after the InterceptorChain#proceed invocation
    }
}

在message、payload和InterceptorChain旁边,@MessageHandlerInterceptor注释方法也可以解析其他参数。框架可以解析此类函数的哪些参数取决于拦截器处理的消息类型。有关哪些参数可用于处理的消息的详细信息,请查看此页。

@ExceptionHandler

@MessageHandlerInterceptor也允许更特定版本的拦截函数。即带@ExceptionHandler注释的方法。用@ExceptionHandler注释的函数将被视为一个处理程序拦截器,它只会在出现异常结果时被调用。例如,为此使用带注释的函数,可以作为引发的数据库/服务异常的结果引发更特定于域的异常。您可以引入对所有异常作出反应的@ExceptionHandler,或者指定带注释的方法对哪些异常做出反应,如下所示:

class GiftCard {

    // State, command handlers and event sourcing handlers omitted for brevity

    @ExceptionHandler
    public void handleAll(Exception exception) {
        // Handles all exceptions thrown within this component generically
    }

    @ExceptionHandler
    public void handleIssueCardExceptions(IssueCardCommand command) {
        // Handles all exceptions thrown from the IssueCardCommand handler within this component
    }

    @ExceptionHandler(payloadType = IssueCardCommand.class)
    public void handleIssueCardExceptions() {
        // Handles all exceptions thrown from the IssueCardCommand handler within this component
    }

    @ExceptionHandler
    public void handleIllegalStateExceptions(IllegalStateException exception) {
        // Handles all IllegalStateExceptions thrown within this component
    }

    @ExceptionHandler(resultType = IllegalStateException.class)
    public void handleIllegalStateExceptions(Exception exception) {
        // Handles all IllegalStateExceptions thrown within this component
    }

    @ExceptionHandler
    public void handleIllegalStateExceptionsFromIssueCard(IssueCardCommand command,
                                                          IllegalStateException exception) {
        // Handles all IllegalStateExceptions thrown from the IssueCardCommand handler within this component
    }

    @ExceptionHandler(resultType = IllegalStateException.class, payloadType = IssueCardCommand.class)
    public void handleIllegalStateExceptionsFromIssueCard() {
        // Handles all IllegalStateExceptions thrown from the IssueCardCommand handler within this component
    }
}
class CardSummaryProjection {

    // Repositories/Services, event handlers and query handlers omitted for brevity

    @ExceptionHandler
    public void handleAll(Exception exception) {
        // Handles all exceptions thrown within this component generically
    }

    @ExceptionHandler
    public void handleFindCardQueryExceptions(FindCardQuery query) {
        // Handles all exceptions thrown from the FindCardQuery handler within this component
    }

    @ExceptionHandler(payloadType = FindCardQuery.class)
    public void handleFindCardQueryExceptions() {
        // Handles all exceptions thrown from the FindCardQuery handler within this component
    }

    @ExceptionHandler
    public void handleIllegalArgumentExceptions(IllegalArgumentException exception) {
        // Handles all IllegalArgumentExceptions thrown within this component
    }

    @ExceptionHandler(resultType = IllegalArgumentException.class)
    public void handleIllegalArgumentExceptions(Exception exception) {
        // Handles all IllegalArgumentExceptions thrown within this component
    }

    @ExceptionHandler
    public void handleIllegalArgumentExceptionsFromCardIssued(CardIssuedEvent event,
                                                              IllegalArgumentException exception) {
        // Handles all IllegalArgumentExceptions thrown from the CardIssuedEvent handler within this component
    }

    @ExceptionHandler(resultType = IllegalArgumentException.class, payloadType = CardIssuedEvent.class)
    public void handleIllegalArgumentExceptionsFromCardIssued() {
        // Handles all IllegalArgumentExceptions thrown from the CardIssuedEvent handler within this component
    }
}

3.5一些注解支持的参数

本章提供了注释消息处理函数的所有可能参数的详尽列表。任何消息处理函数的参数都是通过一个叫做ParameterResolver的Axon框架内部机制来解析的。如果应该将自定义(或尚未)支持的参数注入到带注释的处理程序中,则可以扩展parameterResolver集。

支持命令处理的参数

默认情况下,@CommandHandler注释的方法允许以下参数类型:             

※第一个参数是命令消息的有效负载。             

※如果@CommandHandler注释显式定义了处理程序可以处理的命令的名称,那么它也可以是Message或CommandMessage类型。默认情况下,命令名是命令及其负载的完全限定类名。             

※用@MetaDataValue注释的参数将解析为带有注释所示键的元数据值。如果required为false(默认),则在元数据值不存在时传递null。如果required为true,则解析程序将不匹配,并且在元数据值不存在时阻止调用该方法。             

※MetaData类型的参数将注入CommandMessage的整个元数据。             

※UnitOfWork类型的参数获取注入的当前工作单元。这允许命令处理程序注册要在工作单元的特定阶段执行的操作,或获得对向其注册的资源的访问权。             

※类型为Message或CommandMessage的参数将获得完整的消息,其中包含有效负载和元数据。如果一个方法需要多个元数据字段或包装消息的其他属性,这将非常有用。             

※用@MessageIdentifier注释的String类型的参数将解析正在处理的CommandMessage的标识符             

※ConflictResolver类型的参数将解析配置的ConflictResolver实例。有关此主题的详细信息,请参阅冲突解决部分。            

※InterceptorChain类型的参数将解析CommandMessage的messagehandler拦截器链,此特性应与@CommandHandlerInterceptor注释方法结合使用。关于这方面的更多细节,建议阅读本节。             

※如果应用程序在Spring环境中运行,则可以解析任何springbean。请注意,@Qualifier注释可以与此一起使用,以进一步指定应该解析哪个Bean。

支持查询处理的参数

默认情况下,@QueryHandler注释方法允许以下参数类型:             

※第一个参数是查询消息的有效负载。             

※如果@QueryHandler注释明确定义了处理程序可以处理的查询的名称,那么它也可以是Message或QueryMessage类型。默认情况下,查询名称是查询的有效负载的完全限定类名。              

※用@MetaDataValue注释的参数将解析为带有注释所示键的元数据值。如果required为false(默认),则在元数据值不存在时传递null。如果required为true,则解析程序将不匹配,并且在元数据值不存在时阻止调用该方法。             

※MetaData类型的参数将注入QueryMessage的整个元数据。             

※UnitOfWork类型的参数获取注入的当前工作单元。允许处理程序在此阶段注册或访问该单元的特定操作。              

※类型为Message或QueryMessage的参数将获得完整的消息,包括有效负载和元数据。如果一个方法需要多个元数据字段或包装消息的其他属性,这将非常有用。             

※用@MessageIdentifier注释的String类型的参数将解析正在处理的QueryMessage的标识符             

※如果应用程序在Spring环境中运行,则可以解析任何springbean。请注意,@Qualifier注释可以与此一起使用,以进一步指定应该解析哪个Bean。

3.6处理异常

在开发软件时,异常处理是一个众所周知的概念。在分布式应用程序环境中处理异常比我们通常习惯的更具挑战性。尤其是在处理命令或查询时出现的故障,以及打算有返回值的消息时,我们应该意识到如何抛出异常。

3.6.1处理程序执行异常

HandlerExecutionException标记源自消息处理成员的异常。由于事件消息是单向的,因此处理事件时不包含任何返回值。因此,HandlerExecutionException只应作为处理命令的异常结果返回。Axon为失败的命令和查询处理提供了这个异常的更具体的实现,分别是CommandExecutionException和QueryExecutionException。             

在分布式应用程序环境中,专用处理程序执行异常的有用性变得更加明显,例如,有一个专用的应用程序处理命令,另一个应用程序负责查询端。由于应用程序隔离,您失去了两个应用程序可以访问相同类的任何确定性,因此对于任何异常类都是如此。为了支持和鼓励这种分离,Axon将生成任何由命令或查询处理导致的异常。             

为了维护对依赖于分布式场景中抛出的异常类型的条件逻辑的支持,可以在HandlerExecutionException中提供详细信息。因此,建议在命令/查询处理失败时抛出CommandExecutionException/QueryExecutionException,其中包含所需的详细信息。这种行为通常可以通过实现拦截器来支持,这些拦截器为您执行异常包装。

3.6.2 @ExceptionHandler注释方法

Axon框架允许使用带@ExceptionHandler注释的方法来提供关于如何对异常做出反应的更细粒度的控制。更具体地说,这是一种专门用于对异常结果作出反应的消息处理程序侦听器。注意,这样的@ExceptionHandler只处理同一类中消息处理函数抛出的异常。有关如何使用此注释的详细信息,请参阅本节。

3.7工作单元

工作单元(UnitOfWork)是Axon框架中的一个重要概念。不过,在大多数情况下,你不太可能直接与它互动。消息的处理被看作是一个单独的单元。工作单元的目的是协调在处理消息(命令、事件或查询)期间执行的操作。组件可以注册要在UnitOfWork的每个阶段执行的操作,例如onPrepareCommit或onCleanup。            

你不太可能需要直接访问UnitOfWork。它主要由Axon框架提供的构建块使用。如果您确实需要访问它,无论出于什么原因,你有几种方法可以获得它。处理程序通过handle方法中的参数接收工作单元。如果使用注释支持,则可以将UnitOfWork类型的参数添加到带注释的方法中。在其他位置,您可以通过调用CurrentUnitOfWork.get(). 请注意,如果没有UnitOfWork绑定到当前线程,则此方法将引发异常。使用CurrentUnitOfWork.isStarted()看看是否有工作单元启动。             

访问当前工作单元的一个原因是附加在消息处理过程中多次重用的资源,或者如果创建的资源需要在工作单元完成时清理。在这种情况下,unitOfWork.GetorComputerSource()和生命周期回调方法(如onRollback()、afterCommit()和onCleanup()允许您注册资源并声明在处理此工作单元期间要执行的操作。

请注意,工作单元只是变更的缓冲区,而不是事务的替代品。尽管所有阶段化的更改都只在提交工作单元时提交,但它的提交不是原子性的。这意味着,当提交失败时,某些更改可能已被持久化,而其他更改则没有持久化。最好使一个命令只包含单个操作。如果你坚持这种做法,每个工作单元将只包含一个单独的动作,这样它就可以安全地按原样使用。如果工作单元中有更多操作,则可以考虑将事务附加到工作单元的提交。使用unitOfWork.onCommit(..)注册在提交工作单元时需要采取的行动。

您的处理程序可能会由于处理消息而引发异常。默认情况下,未经检查的异常将导致UnitOfWork还原所有更改。这可能不会引发你所设置的边缘动作。

Axon Framework提供了一些现成的回滚策略:

RollbackConfigurationType.NEVER -将始终提交 UnitOfWork

RollbackConfigurationType.ANY_THROWABLE-发生异常时总是回滚

RollbackConfigurationType.UNCHECKED_EXCEPTIONS-将回滚错误和运行时异常

RollbackConfigurationType.RUNTIME_EXCEPTION-将回滚运行时异常(但不会包含错误)

使用框架组件处理消息时,工作单元的生命周期将自动为您管理。如果选择不使用这些组件,而是自己处理,则需要以编程方式启动并提交(或回滚)一个工作单元。             

在大多数情况下,DefaultUnitOfWork将为您提供所需的功能。它期望在单个线程内进行处理。要在工作单元的上下文中执行任务,只需在新的DefaultUnitOfWork上调用UnitOfWork.execute(可运行)或UnitOfWork.executeWithResult(可调用)。工作单元将在任务完成时启动并提交,如果任务失败则回滚。如果需要更多控制,还可以选择手动启动、提交或回滚工作单元。             

典型用法如下:

UnitOfWork uow = DefaultUnitOfWork.startAndGet(message);
// then, either use the autocommit approach:
uow.executeWithResult(() -> ... logic here);

// or manually commit or rollback:
try {
    // business logic comes here
    uow.commit();
} catch (Exception e) {
    uow.rollback(e);
    // maybe rethrow...
}

工作单位包含着信息。它总是以要处理的消息开头。工作单元执行executeWithResult(…)后将返回ResultMessage,而实际执行结果则是该ResultMessage的有效负载。如果在消息处理过程中出现问题,我们将得到一个异常的ResultMessage--isException()方法将返回true,exceptionResult()将为我们提供实际的可丢弃文件,指出出了什么问题。

一个工作单元有几个生命周期。每次进入一个新阶段时,都会通知监听器。             

  • 活动阶段-这是工作单元开始的地方。工作单元通常在这个阶段注册到当前线程(通过CurrentUnitOfWork.set(单位工程)。随后,消息通常在这个阶段由消息处理程序处理。             
  • 提交阶段-消息处理完成后,在提交工作单元之前,将调用onPrepareCommit侦听器。如果一个工作单元绑定到一个事务,则调用onCommit侦听器来提交任何支持事务。当提交成功时,将调用afterCommit侦听器。如果提交或之前的任何步骤失败,则调用onRollback侦听器。消息处理程序结果包含在工作单元的ExecutionResult中(如果可用)。             
  • 清理阶段-释放此工作单元(如锁)所持有的任何资源的阶段。如果嵌套了多个工作单元,则清理阶段将推迟到外部工作单元准备清理。

  可以将消息处理过程视为一个原子过程;它要么完全处理,要么根本不处理。Axon框架使用工作单元来跟踪消息处理程序执行的操作。处理程序完成后,Axon将尝试提交在工作单元中注册的操作。             

可以将事务绑定到工作单元。许多组件(例如CommandBus和QueryBus的实现以及所有异步处理的EventProcessors)都允许您配置TransactionManager。然后,系统将使用此事务管理器创建事务并将其绑定到用于管理消息流程的工作单元。             

当应用程序组件在消息处理的不同阶段(如数据库连接或EntityManager)需要资源时,可以将这些资源附加到UnitOfWork。这个unitOfWork.getResources()方法允许您访问附加到当前工作单元的资源。有几个helper方法可以直接在工作单元上使用,这使得使用资源更容易。

当嵌套的工作单元需要访问资源时,建议将其注册到根工作单元上,可以使用unitOfWork.root(). 如果一个工作单元是根,这个方法就会返回自己。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值