19.Axon框架-快速体验

59 阅读4分钟

快速体验

1.介绍

本章内容是实现官网给的案例,用于快速体验Axon框架的能力

本教程将指导您使用Axon Framework和Axon Server创建一个自行车租赁应用程序

2.官方代码仓库

github.com/axonIQ/bike…

3.环境准备

  • Maven3
  • JDK21
  • SpringBoot3.3.0

4.搭建项目

项目结构

1767615269159.png

模块作用

  • rental:租赁模块
  • core-api:不同模块之间通信所用的消息

父项目依赖

<?xml version="1.0" encoding="UTF-8"?>
<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>
    <!-- Maven坐标 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
        <relativePath/>
    </parent>
    <groupId>cn.sgy</groupId>
    <artifactId>axon-bikerental</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!--打包方式为pom-->
    <packaging>pom</packaging>
    <!--子模块-->
    <modules>
        <module>rental</module>
        <module>core-api</module>
    </modules>
    <!--版本管理-->
    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <java.version>21</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <axon.version>4.9.4</axon.version>
        <console-framework-client.version>1.7.3</console-framework-client.version>
    </properties>
    <!--依赖管理-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.axonframework</groupId>
                <artifactId>axon-bom</artifactId>
                <version>${axon.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

core-api模块依赖

<?xml version="1.0" encoding="UTF-8"?>
<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>
    <!--Maven坐标-->
    <parent>
        <groupId>cn.sgy</groupId>
        <artifactId>axon-bikerental</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>core-api</artifactId>

    <!--依赖-->
    <dependencies>
        <dependency>
            <groupId>org.axonframework</groupId>
            <artifactId>axon-modelling</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

rental模块依赖

<?xml version="1.0" encoding="UTF-8"?>
<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>
    <!-- Maven坐标 -->
    <parent>
        <groupId>cn.sgy</groupId>
        <artifactId>axon-bikerental</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>rental</artifactId>
    <!--依赖-->
    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>core-api</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.axonframework</groupId>
            <artifactId>axon-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.axonframework</groupId>
            <artifactId>axon-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-core</artifactId>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>process-aot</id>
                        <goals>
                            <goal>process-aot</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

rental模块配置文件

axon:
  serializer:
    general: jackson

management:
  endpoints:
    web:
      exposure:
        include: '*'
spring:
  application:
    name: Rental Monolith
  datasource:
    url: jdbc:h2:./.db/rental_db;AUTO_SERVER=TRUE
  h2:
    console:
      enabled: true
  jpa:
    hibernate:
      ddl-auto: update

5.实现创建自行车

实现思路

logicdiagram.png

  1. 定义RegisterBikeCommand命令
  2. 接收并验证命令@CommandHandler
  3. BikeRegisteredEvent如果有效,则从该地址发送@CommandHandler
  4. 注册并在我们的命令模型中BikeRegisteredEvent使用@EventSourcingHandler
  5. 在我们的命令模型中创建自行车并分配所创建的自行车的详细信息@EventSourcingHandler

定义RegisterBikeCommand命令

/**
 * 注册自行车命令
 *
 * @author sgy
 */
public record RegisterBikeCommand(@TargetAggregateIdentifier String bikeId,
                                  String bikeType,
                                  String location) {
}

接收并验证命令@CommandHandler

/**
 * 自行车
 * @author sgy
 */
@Aggregate
public class Bike {

    @AggregateIdentifier
    private String bikeId;

    private boolean isAvailable;
    private String reservedBy;
    private boolean reservationConfirmed;

    public Bike() { 
    }

    @CommandHandler
    public Bike(RegisterBikeCommand command) {
        // 发布自行车注册事件
        apply(new BikeRegisteredEvent(command.bikeId(), command.bikeType(), command.location()));
    }
    @EventSourcingHandler
    protected void handle(BikeRegisteredEvent event) {
        this.bikeId = event.bikeId();
        this.isAvailable = true;
    }
}
/**
 * 自行车已注册事件
 *
 * @author sgy
 */
public record BikeRegisteredEvent(String bikeId, String bikeType, String location) {
}

6.使用Docker Compose部署Axon Server

yaml文件

services:
  axonserver:
    image: 'axoniq/axonserver:latest'
    environment:
      - 'AXONIQ_AXONSERVER_STANDALONE=TRUE'
    ports:
      - '8024:8024'
      - '8124:8124'

SpringBoot3.1支持

自SpringBoot3.1版本起,SpringBoot支持在运行应用程序之前启动Docker Compose服务

要启用该功能,我们需要在父项目中添加以下依赖项:

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-docker-compose</artifactId>
		<scope>runtime</scope>
		<optional>true</optional>
	</dependency>
</dependencies>

添加此依赖项后,我们就可以直接从IDE运行应用程序了。SpringBoot将检测到该compose.yaml文件,并启动该文件中描述的Docker容器

7.调用注册自行车接口

调用

省略,使用postman,idea的工具,都可以

POST http://localhost:8080/bikes?bikeType=city&location=Utrecht

使用Axon Server检查信息

1767624306491.png

1767624317640.png

1767624326882.png

8.为@CommandHandler编写单元测试

为rental模块引入依赖

<dependencies>

    <dependency>
        <groupId>org.axonframework</groupId>
        <artifactId>axon-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.axonframework</groupId>
        <artifactId>axon-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

注意:最开始已经引入了,就不用再引入了

编写单元测试

1767624584539.png

/**
 * 租赁测试
 * @author sgy
 */
public class BikeTest {
    private AggregateTestFixture<Bike> fixture;

    @BeforeEach
    void setUp() {
        fixture = new AggregateTestFixture<>(Bike.class);
    }

    @Test
    void canRegisterBike() {
        fixture.givenNoPriorActivity()
                .when(new RegisterBikeCommand("bikeId-1234", "city-bike", "Amsterdam"))
                .expectEvents(new BikeRegisteredEvent("bikeId-1234", "city-bike", "Amsterdam"));
    }
}

运行单元测试与验证

省略

9.创建查询模型

Controller

在RentalController中定义两个方法,一个用来查询所有和查询单个的自行车

   @GetMapping("/bikes")
    public CompletableFuture<List<BikeStatus>> findAll() {
        return queryGateway.query(
                BikeStatusNamedQueries.FIND_ALL,
                null,
                ResponseTypes.multipleInstancesOf(BikeStatus.class)
        );
    }

    @GetMapping("/bikes/{bikeId}")
    public CompletableFuture<BikeStatus> findStatus(@PathVariable("bikeId") String bikeId) {
        return queryGateway.query(BikeStatusNamedQueries.FIND_ONE, bikeId, BikeStatus.class);
    }

定义查询名称

为了让查询正确路由,我们需要定义查询的名称,在core-api的模块下创建如下类:

/**
 * 查询名称
 * @author sgy
 */
public class BikeStatusNamedQueries {
    public static final String FIND_ALL = "findAll";
    public static final String FIND_ONE = "findOne";
    public static final String FIND_AVAILABLE = "findAvailable";
}

定义返回实体

在core-api模块下定义返回实体,具体如下:


@Entity
public class BikeStatus {

    @Id
    private String bikeId;
    private String bikeType;
    private String location;
    private String renter;
    private RentalStatus status;

    public BikeStatus() {
    }

    public BikeStatus(String bikeId, String bikeType, String location) {
        this.bikeId = bikeId;
        this.bikeType = bikeType;
        this.location = location;
        this.status = RentalStatus.AVAILABLE;
    }

    public String getBikeId() {
        return bikeId;
    }

    public String getBikeType() {
        return bikeType;
    }

    public String getLocation() {
        return location;
    }

    public String getRenter() {
        return renter;
    }

    public RentalStatus getStatus() {
        return status;
    }

    public String description() {
        switch (status) {
            case RENTED:
                return String.format("Bike %s was rented by %s in %s", bikeId, renter, location);
            case AVAILABLE:
                return String.format("Bike %s is available for rental in %s.", bikeId, location);
            case REQUESTED:
                return String.format("Bike %s is requested by %s in %s", bikeId, renter, location);
            default:
                return "Status unknown";
        }
    }

    public void returnedAt(String location) {
        this.location = location;
        this.status = RentalStatus.AVAILABLE;
        this.renter = null;
    }


    public void requestedBy(String renter) {
        this.renter = renter;
        this.status = RentalStatus.REQUESTED;
    }

    public void rentedBy(String renter) {
        this.renter = renter;
        this.status = RentalStatus.RENTED;
    }
}

创建投影


/**
 * 自行车查询投影
 * @author sgy
 */
@Component
public class BikeStatusProjection {
    private final BikeStatusRepository bikeStatusRepository;

    public BikeStatusProjection(BikeStatusRepository repository) {
        this.bikeStatusRepository = repository;
    }

    /**
     * 监听自行车注册事件,使用JPA保存进去
     * @param event
     */
    @EventHandler
    public void on(BikeRegisteredEvent event) {
        var bikeStatus = new BikeStatus(event.bikeId(), event.bikeType(), event.location());
        bikeStatusRepository.save(bikeStatus);
    }
    @QueryHandler(queryName = BikeStatusNamedQueries.FIND_ALL)
    public Iterable<BikeStatus> findAll() {
        return bikeStatusRepository.findAll();
    }
    @QueryHandler(queryName = BikeStatusNamedQueries.FIND_AVAILABLE)
    public Iterable<BikeStatus> findAvailable(String bikeType) {
        return bikeStatusRepository.findAllByBikeTypeAndStatus(bikeType, RentalStatus.AVAILABLE);
    }

    @QueryHandler(queryName = BikeStatusNamedQueries.FIND_ONE)
    public BikeStatus findOne(String bikeId) {
        return bikeStatusRepository.findById(bikeId).orElse(null);
    }
}

创建JPA的Repository


@Repository
public interface BikeStatusRepository
        extends JpaRepository<BikeStatus, String> { //<.>
    List<BikeStatus> findAllByBikeTypeAndStatus(String bikeType, RentalStatus status);
    long countBikeStatusesByBikeType(String bikeType);
}

调用查询接口

省略