Основні налаштування для сервера Axon

pic

Розуміння основ є критично важливим, адже коли ви їх освоїте, зможете застосувати ці ж концепції до складніших сценаріїв, якщо вони базуються на тих фундаментальних ідеях, які ви вже розумієте. У цьому посібнику ми застосуємо практичний підхід до налаштування Axon Server, що допоможе вам зрозуміти ці основні концепції.

Цей посібник допоможе вам оцінити:

  1. CQRS Архітектура (Command Query Responsibility Segregation)
  2. Проєктування, орієнтоване на домен (DDD)
  3. Проєктування, орієнтоване на події (EDD)

Ми глибше дослідимо ці концепції по ходу виконання проєкту. Однак, щоб почати, ось короткий огляд:

Розуміння архітектури CQRS

CQRS означає Command Query Responsibility Segregation — шаблон проєктування, в якому:

  • Команди (наприклад, створення, оновлення, видалення) викликають події, що змінюють стан системи.
  • Запити відповідають за отримання даних без зміни стану.

Це розмежування дозволяє обробляти операції читання та запису незалежно одна від одної.

Проєктування, орієнтоване на домен (DDD)

В DDD система розбивається на домени, і кожен домен має свою логіку, що інкапсульована в окремому пакеті. Це контрастує з традиційним підходом, де класи різних доменів можуть співіснувати в таких пакетах, як контролери, сервіси або репозиторії.

Наприклад, у системі оренди автомобілів сутності, як Car і Person, організовуються в свої відповідні пакети, що забезпечує модульність і кращу підтримуваність.

Проєктування, орієнтоване на події (EDD)

В орієнтованій на події системі замість того, щоб зберігати стан сутностей безпосередньо в базі даних, ми зберігаємо події, пов'язані з цими сутностями. Щоб визначити поточний стан сутності, система відтворює події, пов'язані з нею. Сутність представлена як агрегатний клас, який надає фінальний стан об'єкта.

Робота з багатомодульними проєктами

Для підвищення модульності ми будемо використовувати багатомодульну структуру проєкту Maven, а не упаковувати все в один модуль. Такий підхід робить наш додаток масштабованим і легким для розширення.

Починаємо: Додаток для оренди автомобілів

Ми розробимо додаток Car Rental, де:

  1. Автомобіль можна зареєструвати.
  2. Можна отримати список зареєстрованих автомобілів або деталі конкретного автомобіля.

Цей проєкт буде базовою основою, що дозволить нам надалі розвивати більш складні функції.

Вимоги

Перед тим як почати, переконайтеся, що у вас встановлено наступне:

  1. Maven
  • Maven є необхідним для побудови та управління Java проєктами. Якщо він не встановлений, скористайтеся вашим улюбленим методом для його налаштування. Після встановлення переконайтеся, що команда mvn працює в вашому терміналі.
  1. Docker
  • Docker необхідний для запуску Axon Server як окремого сервера. Ми будемо використовувати Docker Compose для завантаження та запуску образу Axon Server з Docker Hub.

Структура багатомодульного проєкту

Проєкт матиме таку структуру:

📒 car-rental  
| - - 📒 core-api  
| - - - - - - - - - 📂 src  
| - - - - - - - - - - - - -📂 main  
| - - - - - - - - - - - - - - - - -📂 java  
| - - - - - - - - - - - - - - - - - - - - -📦 io.axoniq.demo.carrental.coreapi  
| - - - - - - - - - 📄 pom.xml  
| - - 📒 rental  
| - - - - - - - - - 📂 src  
| - - - - - - - - - - - - -📂 main  
| - - - - - - - - - - - - - - - - -📂 java  
| - - - - - - - - - - - - - - - - - - - - -📦 io.axoniq.demo.carrental.rental  
| - - - - - - - - - 📄 pom.xml  
| - - 📄 pom.xml

Крок 1: Створення кореневого модуля

Ми почнемо з створення кореневого модуля за допомогою Maven. Виконайте наступну команду у вашому терміналі:

~~
-DarchetypeArtifactId=pom-root \
-DarchetypeVersion=RELEASE \
-DgroupId=io.axoniq.demo.carrental \
-DartifactId=car-rental \
-Dversion=0.0.1-SNAPSHOT \
-DinteractiveMode=false
```

Це створить кореневий модуль для нашого проєкту.

Крок 2: Налаштування батьківського POM

Після імпорту проєкту додайте наступний тег на початку вашого pom.xml (під тегом ):


org.springframework.boot  
spring-boot-starter-parent  
3.3.0  

Це забезпечить успадкування необхідних залежностей та налаштувань від батьківського Spring Boot starter.

Додайте наступне під тегом у вашому pom.xml:


21  
21  
21  

Час додати підмодулі

Не забудьте перезавантажити ваш Maven проєкт у IDE після внесення змін.

2. Додавання двох підмодулів

Модуль оренди
Перейдіть у термінал вашої IDE та виконайте наступну команду в один рядок:

mvn archetype:generate -DgroupId=io.axoniq.demo.carrental -DartifactId=rental -Dpackage=io.axoniq.demo.carrental.rental -DinteractiveMode=false

Модуль Core-API
Виконайте наступну команду для створення модуля Core-API:

mvn archetype:generate \  
-DgroupId=io.axoniq.demo.carrental \  
-DartifactId=core-api \  
-Dpackage=io.axoniq.demo.carrental.coreapi \  
-DinteractiveMode=false

Після цього ваша структура проєкту повинна виглядати так:

project-root/  
├── core-api/  
│ ├── pom.xml  
│ └── src/  
├── rental/  
│ ├── pom.xml  
│ └── src/  
└── pom.xml (root)

3. Очищення залежностей

Модуль оренди залежатиме від модуля core-api, тому потрібно додати core-api як залежність у pom.xml модуля оренди:

pom.xml (оренда):


${project.groupId}  
core-api  
${project.version}  

Далі додайте залежність від Axon Framework у кореневому pom.xml, використовуючи Maven BOM (Bill of Materials). Maven BOM дозволяє вказувати версії компонентів централізовано, але залежності включаються тільки тоді, коли явно вказані в підмодулях.

Більше інформації:
Bill of Materials (BOM) у Maven містить перевірені сумісні компоненти, бібліотеки та їх версії. Ці компоненти не включаються, поки не будуть явно вказані в підмодулях.

Додати версію Axon Framework
Додайте наступне в розділ кореневого pom.xml:

4.9.4

Додати залежність Axon BOM
Додайте це під розділ кореневого pom.xml:




org.axonframework  
axon-bom  
${axon.version}  
pom  
import  



Залежності модуля оренди
Оновіть pom.xml модуля оренди, щоб додати необхідні залежності Axon:



org.axonframework  
axon-spring-boot-starter  


org.axonframework  
axon-test  
test  


Залежності модуля Core-API
Модуль core-api буде служити як модуль обміну повідомленнями для сервера Axon. Оновіть pom.xml модуля core-api таким чином:



org.axonframework  
axon-modelling  


org.springframework.boot  
spring-boot-starter-data-jpa  


Примітка: Залежність JPA додана для підтримки моделювання повідомлень та проекцій. Ми розглянемо її використання пізніше.

Ось це і є гарне очищення!

4.

Налаштування WebMVC

Оскільки ми будемо використовувати Swagger та базу даних H2 в пам'яті, нам потрібно налаштувати WebMVC. Це важливо, оскільки наш додаток є реактивним, і використовує WebFlux. WebFlux інколи може мати проблеми з відображенням не-реактивних сторінок, згенерованих WebMVC, таких як сторінка консолі H2 або документація OpenAPI від Swagger.

Хоча Spring OpenAPI пропонує альтернативну залежність спеціально для WebFlux, ми все одно хочемо, щоб консоль бази даних H2 працювала бездоганно. Тому ми налаштуємо WebMVC, щоб усе працювало коректно.

Це налаштування просте і вимагає лише кілька кроків:

Крок 1: Додавання залежностей

Додайте наступні залежності в файл pom.xml модуля оренди:


org.springframework.boot  
spring-boot-starter-data-jpa  


com.h2database  
h2  
runtime  


org.springframework.boot  
spring-boot-starter-webflux  


org.springframework.boot  
spring-boot-starter-actuator  


org.springdoc  
springdoc-openapi-starter-webmvc-ui  
2.1.0  


org.springframework.boot  
spring-boot-starter-web  

Крок 2: Створення пакету config

У модулі оренди створіть пакет з ім'ям config.

Крок 3: Додавання класів конфігурації

У пакеті config створіть два наступні класи:

SwaggerConfig.java:

@Configuration  
public class SwaggerConfig {  
@Bean  
public GroupedOpenApi publicApi() {  
return GroupedOpenApi.builder()  
.group("carrental-public")  
.pathsToMatch("/**")  
.build();  
}  
@Bean  
public OpenAPI customOpenAPI() {  
return new OpenAPI()  
.info(new Info()  
.title("Spring Boot REST API")  
.version("1.0")  
.description("API документація для вашого Spring Boot додатку"));  
 }  
}

WebConfig.java:

@Configuration  
@EnableWebMvc  
public class WebConfig implements WebMvcConfigurer {}

5. Пакети та головна точка входу

Перед написанням коду давайте організуємо структуру проєкту та налаштуємо точку входу:

Пакети модуля оренди

У модулі оренди створіть наступні пакети:

  • command
  • query
  • controller
  • Існуючий пакет config

Пакет модуля Core-API

У модулі core-api створіть єдиний пакет з ім'ям rental.

Головна точка входу

Нам потрібна тільки одна точка входу між двома модулями. Тому:

  1. Видаліть клас App у модулі core-api (якщо він є).
  2. Змініть клас App у модулі оренди, щоб він виглядав ось так:

App.java:

@SpringBootApplication  
public class App {  
public static void main(String[] args) {  
SpringApplication.run(App.class, args);  
 }  
}

6. Налаштування властивостей додатку

Створення папки resources в модулі оренди

Оскільки папка resources відсутня, виконайте наступні кроки для її створення:

  1. Клацніть правою кнопкою миші на головній папці в модулі оренди.
  2. Виберіть New > Directory.
  3. З'явиться діалогове вікно. Знайдіть опцію resources і натисніть на неї. Це створить папку resources.
    4.
    ## Налаштування WebMVC

Оскільки ми будемо використовувати Swagger та базу даних H2 в пам'яті, нам потрібно налаштувати WebMVC. Це важливо, оскільки наш додаток є реактивним, і використовує WebFlux. WebFlux інколи може мати проблеми з відображенням не-реактивних сторінок, згенерованих WebMVC, таких як сторінка консолі H2 або документація OpenAPI від Swagger.

Хоча Spring OpenAPI пропонує альтернативну залежність спеціально для WebFlux, ми все одно хочемо, щоб консоль бази даних H2 працювала бездоганно. Тому ми налаштуємо WebMVC, щоб усе працювало коректно.

Це налаштування просте і вимагає лише кілька кроків:

Крок 1: Додавання залежностей

Додайте наступні залежності в файл pom.xml модуля оренди:


org.springframework.boot  
spring-boot-starter-data-jpa  


com.h2database  
h2  
runtime  


org.springframework.boot  
spring-boot-starter-webflux  


org.springframework.boot  
spring-boot-starter-actuator  


org.springdoc  
springdoc-openapi-starter-webmvc-ui  
2.1.0  


org.springframework.boot  
spring-boot-starter-web  

Крок 2: Створення пакету config

У модулі оренди створіть пакет з ім'ям config.

Крок 3: Додавання класів конфігурації

У пакеті config створіть два наступні класи:

SwaggerConfig.java:

@Configuration  
public class SwaggerConfig {  
@Bean  
public GroupedOpenApi publicApi() {  
return GroupedOpenApi.builder()  
.group("carrental-public")  
.pathsToMatch("/**")  
.build();  
}  
@Bean  
public OpenAPI customOpenAPI() {  
return new OpenAPI()  
.info(new Info()  
.title("Spring Boot REST API")  
.version("1.0")  
.description("API документація для вашого Spring Boot додатку"));  
 }  
}

WebConfig.java:

@Configuration  
@EnableWebMvc  
public class WebConfig implements WebMvcConfigurer {}

5. Пакети та головна точка входу

Перед написанням коду давайте організуємо структуру проєкту та налаштуємо точку входу:

Пакети модуля оренди

У модулі оренди створіть наступні пакети:

  • command
  • query
  • controller
  • Існуючий пакет config

Пакет модуля Core-API

У модулі core-api створіть єдиний пакет з ім'ям rental.

Головна точка входу

Нам потрібна тільки одна точка входу між двома модулями. Тому:

  1. Видаліть клас App у модулі core-api (якщо він є).
  2. Змініть клас App у модулі оренди, щоб він виглядав ось так:

App.java:

@SpringBootApplication  
public class App {  
public static void main(String[] args) {  
SpringApplication.run(App.class, args);  
 }  
}

6. Налаштування властивостей додатку

Створення папки resources в модулі оренди

Оскільки папка resources відсутня, виконайте наступні кроки для її створення:

  1. Клацніть правою кнопкою миші на головній папці в модулі оренди.
  2. Виберіть New > Directory.
  3. З'явиться діалогове вікно. Знайдіть опцію resources і натисніть на неї. Це створить папку resources.
  4. Всередині папки resources створіть новий файл з ім'ям application.properties.

Налаштування application.properties

Додайте наступну конфігурацію в файл application.properties:

# Назва додатку  
spring.application.name=Модуль оренди  
# Налаштування H2 бази даних  
spring.datasource.url=jdbc:h2:mem:testdb  
spring.datasource.driver-class-name=org.h2.Driver  
spring.h2.console.enabled=true  
spring.h2.console.path=/h2-console  
spring.datasource.username=sa  
spring.datasource.password=password  
# Налаштування JPA/Hibernate  
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect  
spring.jpa.hibernate.ddl-auto=update  
# Налаштування Axon Server  
axon.serializer.general=jackson

7. Налаштування модуля Core-API

У пакеті оренди модуля Core-api, створіть наступні класи, перерахування та записи:

A. CarRegisteredEvent (Record)

Представляє подію реєстрації автомобіля.

public record CarRegisteredEvent(String carId, String carBrand, String location) {}

B. CarRegistrationCommand (Record)

Представляє команду для реєстрації автомобіля.

public record CarRegistrationCommand(  
@TargetAggregateIdentifier String carId,  
String carBrand,  
String location  
) {}

C. RentalStatus (Enum)

Окреслює різні стани оренди.

public enum RentalStatus {  
AVAILABLE,  
REQUESTED,  
RENTED  
}

D. CarStatus (Entity)

Представляє статус автомобіля, включаючи його стан оренди.

@Entity  
@Getter  
@NoArgsConstructor  
@AllArgsConstructor  
public class CarStatus {  
@Id  
private String carId;  
private String carBrand;  
private String location;  
private String renter;  
private RentalStatus status;  
public CarStatus(String carId, String carBrand, String location) {  
this.carId = carId;  
this.carBrand = carBrand;  
this.location = location;  
this.status = RentalStatus.AVAILABLE;  
 }  
}

Примітка: Lombok використовується для спрощення коду, щоб уникнути шаблонних геттерів та конструкторів. Переконайтеся, що Lombok доданий як залежність у вашому pom.xml.

E. Додати залежність Lombok

У pom.xml модуля Core-api додайте наступне:


org.projectlombok  
lombok  
true  

F. CarStatusList (Record)

Представляє список об'єктів CarStatus.

public record CarStatusList(List entries) {}

G. CarStatusNamedQueries (Class)

Містить іменовані запити для отримання інформації про статус автомобіля.

public class CarStatusNamedQueries {  
public static final String FIND_ALL = "findAll";  
public static final String FIND_ONE = "findOne";  
}

Чому використовується залежність JPA в архітектурі, орієнтованій на події?

Хоча ми використовуємо архітектуру, орієнтовану на події, де автомобіль обробляється як агрегат (відтворюючи події для досягнення певного стану), ми все одно використовуємо JPA для збереження стану певних об'єктів (CarStatus). Це дозволяє нам ефективно зберігати та отримувати їхні стани. Однак сам агрегат (Car) не зберігається в базі даних, а відтворює події для визначення свого стану.

Ось і все для налаштування модуля Core-API!

Тепер у вас є базова структура для модуля Core-api, включаючи команди, події, перерахування та сутності. Перейдемо до реалізації основної логіки! 🚀

8. Сканування сутності CarStatus

Сутність CarStatus, визначена в модулі Core-api, використовується в модулі оренди, а не в самому Core-api. Оскільки сутність знаходиться в іншому модулі, ми повинні явно вказати її місце за допомогою @EntityScan у точці входу модуля оренди.

Оновлення App.java (модуль оренди)

@EntityScan(basePackageClasses = {CarStatus.class, SagaEntry.class, TokenEntry.class})  
@SpringBootApplication  
public class App {  
public static void main(String[] args) {  
SpringApplication.run(App.class, args);  
 }  
}

9. Пакет Command у модулі оренди

A.

Додавання залежностей Axon до pom.xml (модуль оренди)

Додайте залежності Axon framework для дозволу обробки команд та подій:


org.axonframework  
axon-spring-boot-starter  


org.axonframework  
axon-test  
test  

Створення класу агрегату Car

Клас Car є агрегатом в архітектурі CQRS та Event Sourcing, який представляє бізнес-логіку для обробки команд і подій, пов'язаних з автомобілями.

@Aggregate  
public class Car {  
@AggregateIdentifier  
private String carId;  
private boolean isAvailable;  
private String reservedBy;  
private boolean reservationConfirmed;  
public Car() {  
// Конструктор за замовчуванням, необхідний для Axon  
}  
@CommandHandler  
public Car(CarRegistrationCommand command) {  
// Генерація події CarRegisteredEvent, коли обробляється команда реєстрації  
apply(new CarRegisteredEvent(command.carId(), command.carBrand(), command.location()));  
}  
@EventSourcingHandler  
protected void handle(CarRegisteredEvent event) {  
// Оновлення стану агрегату на основі події  
this.carId = event.carId();  
this.isAvailable = true;  
 }  
}

Створення класу RentalController

Клас RentalController служить точкою входу для обробки запитів REST API. Він використовує CommandGateway для відправлення команд і QueryGateway для запитів.

@RestController  
@RequestMapping("/")  
@RequiredArgsConstructor  
public class RentalController {  
private final CommandGateway commandGateway;  
private final QueryGateway queryGateway;  
@PostMapping("/cars")  
public CompletableFuture registerCar(  
@RequestParam("carBrand") String carBrand,  
@RequestParam("location") String location) {  
CarRegistrationCommand registerCarCommand =  
new CarRegistrationCommand(  
UUID.randomUUID().toString(),  
carBrand,  
location);  
return commandGateway.send(registerCarCommand);  
 }  
}

Примітка: Анотація @RequiredArgsConstructor від Lombok використовується для генерації конструктора для всіх фінальних полів.

10. Додавання залежності Lombok до pom.xml (модуль оренди)

Додайте залежність Lombok до модуля оренди, як ви це робили для модуля Core-api:


org.projectlombok  
lombok  
true  

Це налаштування створює основу для управління життєвим циклом автомобіля в модулі оренди за допомогою патернів CQRS та Event Sourcing. 🚀

11. Docker Compose та образ Axon Server

Щоб завантажити образ Axon Server для Docker, створіть файл compose.yaml у кореневій папці додатку.

Compose.yaml (корінь)

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

Після створення файлу виконайте наступну команду в вашому терміналі або командному рядку:

docker-compose up

Після того як образ Axon Server буде успішно завантажено та запущено, ви можете зупинити його, оскільки наступного разу ми хочемо, щоб він стартував автоматично під час запуску додатку.

12. Запуск додатку

Щоб запустити додаток разом з образом Docker, додайте наступну залежність у файл pom.xml модуля оренди:

Pom.xml (оренда)


org.springframework.boot  
spring-boot-docker-compose  
runtime  
true  

Нарешті, запустіть ваш додаток, змінивши конфігурацію IDE, щоб вибрати модуль оренди.

Ви можете протестувати один кінцевий точку на даний момент: точку реєстрації автомобіля. Вона повертає carId при реєстрації, що є UUID.

Примітка: Це ще не зберігається в базі даних, оскільки клас репозиторію ще не створений. Події зберігаються в пам'яті самим сервером Axon.

13.

Додавання запиту

Запити, як визначено вище, є командами тільки для читання, які не змінюють сутність і тому не створюють подій. Методи, як get і getAll, належать до цієї категорії.

Спочатку створимо інтерфейс репозиторію для отримання даних.

В середині пакету query створіть інтерфейс CarStatusRepository:

CarStatusRepository.java

@Repository  
public interface CarStatusRepository extends JpaRepository {}

Далі створимо пакет projection.

В архітектурі CQRS, проекції виступають як посередники між подіями, записаними в Axon Server, і базою даних. Вони проектують кожну подію, записану в Axon Server, в базу даних.

В середині пакету query створіть клас CarStatusProjection:

CarStatusProjection.java

@Component  
@RequiredArgsConstructor  
public class CarStatusProjection {  
 private final CarStatusRepository carStatusRepository;  
 private final JdbcTemplate jdbcTemplate;  
 @EventHandler  
 public void on(CarRegisteredEvent event) {  
 var carStatus = new CarStatus(event.carId(), event.carBrand(), event.location());  
 carStatusRepository.save(carStatus);  
 }  
 @QueryHandler(queryName = CarStatusNamedQueries.FIND_ALL)  
 public CarStatusList findAll() {  
 return new CarStatusList(jdbcTemplate.query(  
 "SELECT * FROM car_status",  
 (rs, rowNum) -> new CarStatus(  
 rs.getString("car_id"),  
 rs.getString("car_brand"),  
 rs.getString("location")  
 )  
 ));  
 }  
 @QueryHandler(queryName = CarStatusNamedQueries.FIND_ONE)  
 public CarStatus findOne(String carId) {  
 return carStatusRepository.findById(carId).orElse(null);  
 }  
}
  • Метод @EventHandler відповідає за проекцію події в базу даних.
  • Метод @QueryHandler findAll отримує всі події автомобілів з бази даних.

Чому не використовувати звичайний метод findAll?
Під час налаштування Axon Server з Spring Boot я зіткнувся з проблемою, де очікуваний тип відповіді був List, але фактична відповідь була ArrayList. Проблема виникла через серіалізацію Jackson, яка не могла правильно конвертувати ці типи через проблему невідповідності типів. Обгортання списку в інший об'єкт дозволило вирішити цю проблему, забезпечивши правильне перетворення типів бази даних. Ось чому в цьому випадку використовується JdbcTemplate.

  • Метод findOne є простим — він отримує подію за її ID.

13. Останнє оновлення класу RentalController

Оновіть клас RentalController, щоб включити нові сервіси запитів:

RentalController.java

@GetMapping("/cars")  
public CompletableFuture findAll() {  
 return queryGateway.query(  
 CarStatusNamedQueries.FIND_ALL,  
 null,  
 ResponseTypes.instanceOf(CarStatusList.class)  
 ).exceptionally(ex -> {  
 // Логування або обробка помилки  
 throw new RuntimeException("Не вдалося отримати автомобілі", ex);  
 });  
}  
@GetMapping("/cars/{carId}")  
public CompletableFuture findStatus(@PathVariable("carId") String carId) {  
 return queryGateway.query(CarStatusNamedQueries.FIND_ONE, carId, CarStatus.class);  
}

Тепер ви можете знову запустити додаток.

Підсумки

В цьому посібнику ми розглянули, як налаштувати Axon Server за допомогою Docker Compose для архітектури, що орієнтована на події, в додатку Spring Boot. Ми пройшли через створення необхідного файлу compose.yaml, завантаження образу Axon Server і запуск додатку поряд з Docker контейнером. Крім того, ми реалізували обробку подій та запитів за допомогою CQRS, де команди створюють події, що змінюють стан, а запити отримують дані з бази даних без модифікації сутностей. Останнім кроком ми оновили клас RentalController, щоб виставити як команди, так і запити через REST ендпоінти, що забезпечує плавну реєстрацію автомобілів та отримання інформації про стан автомобіля.
Цей підхід не лише спрощує архітектуру, але й покращує масштабованість та продуктивність для керування даними оренди автомобілів у реальному часі.

🎉 ВІТАЄМО, ВИ ДОСТИГЛИ КІНЦЯ! 🚀

джерело коду: https://github.com/samuelgbenga/axonFrameWorkTemplateCar.git

Перекладено з: Fundamental Setup for Axon Server

Leave a Reply

Your email address will not be published. Required fields are marked *