Component Message Intercepting

Declarative and annotated interceptor support for Message Handling Components will be introduced in 5.2.0. As such, the description below is not complete nor finalized. For those curious, be sure to follow the issue that will implement this functionality.

@CommandHandlerInterceptor annotation

You can add a handler interceptor as a @CommandHandlerInterceptor annotated method on the aggregate or entity. This allows you to make decisions based on the current state of the aggregate:

public class GiftCard {

    private String state;

    @CommandHandlerInterceptor
    public void intercept(RedeemCardCommand command, InterceptorChain chain) {
        if (this.state.equals(command.getState())) {
            chain.proceed();
        }
        // Otherwise, don't proceed - command is blocked
    }

    @CommandHandler
    public void handle(RedeemCardCommand command) {
        // Handle the command
    }
}

Note that the @CommandHandlerInterceptor is essentially a more specific implementation of the @MessageHandlerInterceptor described below.

Annotated @MessageHandlerInterceptor

Alongside defining overall MessageHandlerInterceptor instances on the component handling a message, you can also define a handler interceptor for a specific component containing the handlers. This is achieved by adding a method with the @MessageHandlerInterceptor annotation.

Several options are available:

  1. InterceptorChain parameter: The InterceptorChain is an optional parameter which can be added to the intercepting method. In absence of this parameter, the framework will call InterceptorChain#proceed once the method exits.

  2. Message type filtering: You can define the type of Message the interceptor should deal with using the messageType parameter.

  3. Payload type filtering: For fine-grained control, specify the payloadType contained in the Message.

Simple @MessageHandlerInterceptor method:

public class CardSummaryProjection {

    @MessageHandlerInterceptor
    public void intercept(Message message) {
        // Intercepts all messages to this component
    }

    @EventHandler
    public void on(CardIssuedEvent event) {
        // Handle event
    }
}

Defining the message type:

public class CardSummaryProjection {

    @MessageHandlerInterceptor(messageType = EventMessage.class)
    public void intercept(EventMessage event) {
        // Intercepts only event messages
    }
}

Defining message and payload type:

public class CardSummaryProjection {

    @MessageHandlerInterceptor(
        messageType = EventMessage.class,
        payloadType = CardRedeemedEvent.class
    )
    public void intercept(CardRedeemedEvent event) {
        // Intercepts only CardRedeemedEvent messages
    }
}

Using InterceptorChain for control:

public class CardSummaryProjection {

    @MessageHandlerInterceptor(messageType = QueryMessage.class)
    public void intercept(QueryMessage query, InterceptorChain chain) throws Exception {
        // Logic before handling
        chain.proceed();
        // Logic after handling
    }
}

Hierarchy on aggregate member and root:

public class GiftCard {

    @AggregateIdentifier
    private String id;

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

    @MessageHandlerInterceptor
    public void intercept(Message message) {
        // This interceptor will be invoked FIRST
    }
}

public class GiftCardTransaction {

    @EntityId
    private String transactionId;

    @MessageHandlerInterceptor
    public void intercept(Message message) {
        // This interceptor will be invoked SECOND
    }
}
@MessageHandlerInterceptor hierarchy for @EventSourcingHandler

@MessageHandlerInterceptor follows the aggregate hierarchy when applicable. If an aggregate root has an interceptor, as well as one or several entities, the interceptor chain will "move up" the hierarchy.

However, this support does not exist for event sourcing handlers! Only the interceptors on that level of the hierarchy are invoked. In the example above, when the GiftCardTransaction is event sourced, only its interceptor is invoked, disregarding the one on the GiftCard class.

This is a known discrepancy that cannot be easily adjusted in Axon’s current structure. It is not recommended to rely on an invocation hierarchy for @MessageHandlerInterceptor annotated methods while sourcing an aggregate.

Next to the message, payload, and InterceptorChain, a @MessageHandlerInterceptor annotated method can resolve other parameters as well. Which parameters the framework can resolve depends on the type of Message being handled by the interceptor. For more specifics, see Supported Parameters.

@ExceptionHandler

The @MessageHandlerInterceptor also allows for a more specific version: an @ExceptionHandler annotated method.

The framework invokes @ExceptionHandler annotated methods only for exceptional results of message handling. This allows you to throw more domain-specific exceptions or translate technical exceptions to business errors.

You can wire all default parameters to an exception handler, similar to command, event, and query handlers. Hence, you can add the exception, payload, metadata, and other options.

You can introduce @ExceptionHandler annotated methods in any message handling component, including sagas. You can choose to react to all exceptions or define specific exception/message combinations.

Aggregate exception handlers:

class GiftCard {

    // State, command handlers and event sourcing handlers omitted

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

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

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

    @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 handleSpecificCase(IssueCardCommand command,
                                   IllegalStateException exception) {
        // Handles IllegalStateExceptions from IssueCardCommand handler
    }

    @ExceptionHandler(
        resultType = IllegalStateException.class,
        payloadType = IssueCardCommand.class
    )
    public void handleSpecificCase() {
        // Handles IllegalStateExceptions from IssueCardCommand handler
    }
}
Exception Handling for Aggregate Constructors

The @ExceptionHandler annotated methods require an existing component instance to work. Because of this, exception handlers do not work for (command handling) constructors of an aggregate.

If you expect exceptions on an aggregate’s constructor that you need to handle differently, use Axon’s creation policy.

Projector exception handlers:

class CardSummaryProjection {

    // Event handlers and query handlers omitted

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

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

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

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

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

    @ExceptionHandler
    public void handleSpecificCase(CardIssuedEvent event,
                                   IllegalArgumentException exception) {
        // Handles IllegalArgumentExceptions from CardIssuedEvent handler
    }

    @ExceptionHandler(
        resultType = IllegalArgumentException.class,
        payloadType = CardIssuedEvent.class
    )
    public void handleSpecificCase() {
        // Handles IllegalArgumentExceptions from CardIssuedEvent handler
    }
}