5
votes

I am creating a simple app using Axon + Spring Boot, just to make sure I understand the basic components in Axon framework before I use it in a real project. There is a method annotated with @CommandHandler within the class TaskAggregate that is supposed to be called when I send a command through the CommandGateway, but after running the app I am getting the exception:

Exception in thread "main" org.axonframework.commandhandling.NoHandlerForCommandException: No handler was subscribed to command [com.xxx.axontest.task.CreateTaskCommand]

As per the documentation, the @CommandHandler annotation should be enough to subscribe the command hander to the command bus. I guess I must be missing something. Could you take a look to below code and point me to the right direction?.

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.xxx</groupId>
    <artifactId>axon-test</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <axon.version>3.0.6</axon.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.7.RELEASE</version>
    </parent>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
                <groupId>org.axonframework</groupId>
                <artifactId>axon-spring-boot-starter</artifactId>
                <version>${axon.version}</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

App.java

package com.xxx.axontest;

import org.axonframework.commandhandling.gateway.CommandGateway;
import org.axonframework.eventsourcing.eventstore.EventStorageEngine;
import org.axonframework.eventsourcing.eventstore.inmemory.InMemoryEventStorageEngine;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;

import com.xxx.axontest.task.CreateTaskCommand;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        ConfigurableApplicationContext configurableApplicationContext = SpringApplication.run(App.class, args);
        CommandGateway commandGateway = configurableApplicationContext.getBean(CommandGateway.class);
        commandGateway.send(new CreateTaskCommand(123, "asd"));
    }

    @Bean
    public EventStorageEngine eventStorageEngine() {
        return new InMemoryEventStorageEngine();
    }

    @Bean
    public AnnotationCommandHandlerBeanPostProcessor 
 annotationCommandHandlerBeanPostProcessor() {
    return new AnnotationCommandHandlerBeanPostProcessor();
    }
}

CreateTaskCommand.java

package com.xxx.axontest.task;

import org.axonframework.commandhandling.TargetAggregateIdentifier;

public class CreateTaskCommand {

    @TargetAggregateIdentifier
    private int taskId;
    private String name;

    public CreateTaskCommand(int taskId, String name) {
        this.taskId = taskId;
        this.name = name;
    }

    public int getTaskId() {
        return taskId;
    }

    public String getName() {
        return name;
    }   
}

TaskCreatedEvent.java

package com.xxx.axontest.task;

import org.axonframework.commandhandling.TargetAggregateIdentifier;

public class TaskCreatedEvent {

    @TargetAggregateIdentifier
    private int taskId;
    private String name;

    public int getTaskId() {
        return taskId;
    }

    public String getName() {
        return name;
    }

}

TaskAggregate.java

package com.xxx.axontest.task;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.commandhandling.model.AggregateIdentifier;
import org.axonframework.commandhandling.model.AggregateLifecycle;
import org.axonframework.eventsourcing.EventSourcingHandler;
import org.axonframework.spring.stereotype.Aggregate;

@AggregateRoot
public class TaskAggregate {

    private Logger logger = LogManager.getLogger(TaskCreatedEvent.class);

    @AggregateIdentifier
    private int taskId;
    private String name;

    @CommandHandler
    public void handleCommand(CreateTaskCommand createTaskCommand) {
        logger.info("Command received");
        AggregateLifecycle.apply(new TaskCreatedEvent());
    }

    @EventSourcingHandler
    public void onTaskCreatedEvent(TaskCreatedEvent taskCreatedEvent) {
        logger.info("Event received");
    }

    public int getTaskId() {
        return taskId;
    }

    public void setTaskId(int taskId) {
        this.taskId = taskId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

Thanks in advance.

4
One important thing: events and commands should be immutable (no setters)Constantin Galbenu
From doc: "CommandHandlers need to be subscribed to a CommandBusin order to receive command"Constantin Galbenu
@ConstantinGalbenu, I removed the setters following your recommendation.Rodney

4 Answers

5
votes

I think you need to annotate your aggregate with @Aggregate rather than @AggregateRoot. @Aggregate as an annotation is both the @AggregateRoot annotation, but is also used by the Spring Boot Axon Starter module to signal that for that class an Aggregate factory and Repository has to be created. Additionally, that'll mean the @CommandHandler annotated functions on your aggregate will also be found and registered to the CommandBus, probably solving the exception you caught.

Otherwise, the webinars on YouTube from Allard for starting an Axon application in version 3 could give you some insight.

But, in short, try switching the @AggregateRoot annotation for @Aggregate :-)

Additionally however, you should be adjusting the @CommandHandler annotated function for the CreateTaskCommand to be a constructor for the TaskAggregate. Lastly, Axon requires you to have a no-arg constructor for you aggregate, so also add a public TaskAggregate() { } constructor in there.

3
votes

Based on the above code, a few remarks:

  • you do not need to provide a AnnotationCommandHandlerBeanPostProcessor. In fact, specifying one may interfere with the regular operation of Axon/Spring Boot autoconfiguration
  • The commands that creates a new Aggregate instance should be placed on a constructor. The is no instance to invoke the method on, yet. Note that you will (also) have to specify a no-arg constructor.
  • The taskId should be set by the @EventSourcingHandler. Getters and Setters do not belong in an (event sourced) Aggregate
  • In Events, you do not need to specify @TargetAggregateIdentifier. They are only means for commands.

I can't explain this exception given the code you provide, but there is a chance that the explicitly defined AnnotationCommandHandlerBeanPostProcessor is in the way.

[Edited] Steven correctly noted the @AggregateRoot annotation. It should be @Aggregate. Above comments are still valid, but not directly related to the exception [/Edited]

1
votes

You also need to register your handler to the command bus. I found this tutorial that should help you. A quick highlight from there:

@Configuration 
public class AppConfiguration { 

    @Bean  
    public SimpleCommandBus commandBus() { 
        SimpleCommandBus simpleCommandBus = new SimpleCommandBus(); 
        // This manually subscribes the command handler: 
        // DebitAccountHandler to the commandbus.  
        simpleCommandBus.subscribe(DebitAccount.class.getName(), new DebitAccountHandler()); 
        return simpleCommandBus;  
    }
}

P.S. One important thing: events and commands should be immutable (no setters)

1
votes

Bases on the comments

I will like to share the modified actual code.

@Aggregate
public class TaskAggregate {

    private Logger logger = LogManager.getLogger(TaskCreatedEvent.class);

    @AggregateIdentifier
    private int taskId;

    private String name;

    TaskAggregate(){ // default constructor needed for axon
    }

    @CommandHandler
    public void TaskAggregate(CreateTaskCommand createTaskCommand) {
        logger.info("Command received");
        AggregateLifecycle.apply(new TaskCreatedEvent());
    }

    @EventSourcingHandler
    public void onTaskCreatedEvent(TaskCreatedEvent taskCreatedEvent) {
        logger.info("Event received");
        this.name = taskCreatedEvent; // use this to set the aggregate meber than get and setter.
    }

}

@CommandHandler annotated function for the CreateTaskCommand to be a constructor for the TaskAggregate. Lastly, Axon requires you to have a no-arg constructor for you aggregate, so also add a public TaskAggregate() { } constructor in there.