快速体验
1.介绍
本章内容是实现官网给的案例,用于快速体验Axon框架的能力
本教程将指导您使用Axon Framework和Axon Server创建一个自行车租赁应用程序
2.官方代码仓库
3.环境准备
- Maven3
- JDK21
- SpringBoot3.3.0
4.搭建项目
项目结构
模块作用
- 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.实现创建自行车
实现思路
- 定义RegisterBikeCommand命令
- 接收并验证命令@CommandHandler
- BikeRegisteredEvent如果有效,则从该地址发送@CommandHandler
- 注册并在我们的命令模型中BikeRegisteredEvent使用@EventSourcingHandler
- 在我们的命令模型中创建自行车并分配所创建的自行车的详细信息@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检查信息
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>
注意:最开始已经引入了,就不用再引入了
编写单元测试
/**
* 租赁测试
* @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);
}
调用查询接口
省略