add source

This commit is contained in:
Daniel Kang
2018-04-20 16:55:07 +09:00
parent fc0e658154
commit db4bcdceae
69 changed files with 3523 additions and 14 deletions

View File

@@ -3,6 +3,24 @@
본 Hans-On Lab에서는 Axon Framework를 활용해서 CQRS와 Event Sourcing에 대해서 실습합니다.
# Pre-Requisite
- git 설치 : https://git-scm.com/book/ko/v1/시작하기-Git-설치
- docker : https://docs.docker.com/install/
- JDK1.8 : http://www.oracle.com/technetwork/java/javase/downloads/index.html
- maven : http://maven.apache.org/download.cgi
- Java IDE(Eclipse, IntelliJ등 )
,,,
//Source Code 다운로드
git clone https://github.com/DannyKang/CQRS-ESwithAxon
//MySql image 다운로드
docker pull mysql
//mongodb image 다운로드
docker pull mongo
,,,
## Axon Framework
@@ -630,17 +648,17 @@ AxonAutoConfiguration 내부에서 CommandBus, EventBus, EventStorageEngine, Ser
![Architecture overview of a CQRS Application](https://blobscdn.gitbook.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-L9ehuf5wnTxVIo9Rle6%2F-L9ei79JpweCtX6Qur65%2F-L9eiEg8i2dLcK2ovEZU%2Fdetailed-architecture-overview.png?generation=1523282680564557&alt=media)
CQRS에서는 Command-Side Repository와 Query-Side Repository를 별도로 가지도록 한다. 이 예제에서는 Command-Side는 MongoDB를, Query는 MySQL을 이용하도록 다.
CQRS에서는 Command-Side Repository와 Query-Side Repository를 별도로 가지도록 한다. 이 예제에서는 Command-Side는 MongoDB를, Query는 MySQL을 이용하도록 합니다.
#### 시나리오
>백오피스의 직원이 쇼핑몰에 신규 상품을 생성하면, 고객이 상품 아이템을 선택해서 주문을 하고 결제를 하는 시나리오다.
>Product (id, name, stock, price)
>상품 추가 프로세스
>CreateProductCommand -> new ProductAggregate instance -> ProductCreatedEvent
>여기서 주로 Event는 과거에 일어난 이벤트로 과거시제를 주로 사용한다.
>Order (id, username, payment, products)
>주문 프로세스
CreateOrderCommand-> new OrderAggregateinstance -> OrderCreatedEvent
>백오피스의 직원이 쇼핑몰에 신규 상품을 생성하면, 고객이 상품 아이템을 선택해서 주문을 하고 결제를 하는 시나리오입니다.
>Product (id, name, stock, price)
>상품 추가 프로세스
>CreateProductCommand -> new ProductAggregate instance -> ProductCreatedEvent
>여기서 주로 Event는 과거에 일어난 이벤트로 과거시제를 주로 사용합니다.
>Order (id, username, payment, products)
>주문 프로세스
>CreateOrderCommand-> new OrderAggregateinstance -> OrderCreatedEvent
### Command-Side
@@ -1050,7 +1068,7 @@ public class OrderProductEntry {
### Hands-On
***MySQL Database 추가 생성 "ProductOrder"***
```
//MySql image 다운로드
docker pull mysql
@@ -1062,12 +1080,12 @@ docker run -p 3306:3306 --name mysql1 -e MYSQL_ROOT_PASSWORD=Welcome1 -d mysql
//mongodb 컨테이너 기동
docker run -p 27017:27017 --name mongodb -d mongo
// MySql 데이터 베이스 생성 CQRS
// MySql 데이터 베이스 생성 ProductOrder
docker exec -it mysql1 bash
$mysql -uroot -p
Enter Password : Welcome1
mysql> create database cqrs; -- Create the new database
mysql> grant all on cqrs.* to 'root'@'localhost';
mysql> create database productorder; -- Create the new database
mysql> grant all on productorder.* to 'root'@'localhost';
select host, user from mysql.user;
@@ -1079,7 +1097,7 @@ docker exec -it mongodb bash
POST http://localhost:8080/product/1?name=SoccerBall&price=10&stock=100
```
curl -X POST http://localhost:8080/product/1?name=SoccerBall&price=10&stock=100
curl -d "name=SoccerBall&price=10&stock=200" http://localhost:8080/product/1
```
@@ -1100,7 +1118,11 @@ JSON
}
3. Query DB
```
$docker exec -it mongodb1
$mongo
> use axon
> show collections
events

1058
README.md.old Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

1
lesson-1/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/.metadata/

11
lesson-1/README.md Normal file
View File

@@ -0,0 +1,11 @@
Lesson-1
---
A very basic sample of Axon framework.
In this Sample, you will learn the basic concept of Axon and CQRS & EventSourcing.
- Command
- Event
- CommandGateway
- Command Handler
- Event Handler

16
lesson-1/pom.xml Normal file
View File

@@ -0,0 +1,16 @@
<?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">
<parent>
<artifactId>sbs-axon</artifactId>
<groupId>com.edi.learn</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>lesson-1</artifactId>
<version>1</version>
</project>

View File

@@ -0,0 +1,34 @@
package com.edi.learn.axon;
import com.edi.learn.axon.command.aggregates.BankAccount;
import com.edi.learn.axon.command.commands.CreateAccountCommand;
import com.edi.learn.axon.command.commands.WithdrawMoneyCommand;
import com.edi.learn.axon.common.domain.AccountId;
import org.axonframework.config.Configuration;
import org.axonframework.config.DefaultConfigurer;
import org.axonframework.eventsourcing.eventstore.inmemory.InMemoryEventStorageEngine;
import org.slf4j.Logger;
import static org.slf4j.LoggerFactory.getLogger;
public class Application {
private static final Logger LOGGER = getLogger(Application.class);
public static void main(String args[]){
Configuration config = DefaultConfigurer.defaultConfiguration()
.configureAggregate(BankAccount.class)
.configureEmbeddedEventStore(c -> new InMemoryEventStorageEngine())
.buildConfiguration();
config.start();
AccountId id = new AccountId();
config.commandGateway().send(new CreateAccountCommand(id, "MyAccount",1000));
config.commandGateway().send(new WithdrawMoneyCommand(id, 500));
config.commandGateway().send(new WithdrawMoneyCommand(id, 500));
/*config.commandBus().dispatch(asCommandMessage(new CreateAccountCommand(id, "MyAccount", 1000)));
config.commandBus().dispatch(asCommandMessage(new WithdrawMoneyCommand(id, 500)));*/
}
}

View File

@@ -0,0 +1,60 @@
package com.edi.learn.axon.command.aggregates;
import com.edi.learn.axon.command.commands.CreateAccountCommand;
import com.edi.learn.axon.command.commands.WithdrawMoneyCommand;
import com.edi.learn.axon.common.domain.AccountId;
import com.edi.learn.axon.common.events.AccountCreatedEvent;
import com.edi.learn.axon.common.events.MoneyWithdrawnEvent;
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.commandhandling.model.AggregateIdentifier;
import org.axonframework.eventhandling.EventHandler;
import org.slf4j.Logger;
import java.math.BigDecimal;
import static org.axonframework.commandhandling.model.AggregateLifecycle.apply;
import static org.slf4j.LoggerFactory.getLogger;
public class BankAccount {
private static final Logger LOGGER = getLogger(BankAccount.class);
@AggregateIdentifier
private AccountId accountId;
private String accountName;
private BigDecimal balance;
public BankAccount() {
}
@CommandHandler
public BankAccount(CreateAccountCommand command){
LOGGER.debug("Construct a new BankAccount");
apply(new AccountCreatedEvent(command.getAccountId(), command.getAccountName(), command.getAmount()));
}
@CommandHandler
public void handle(WithdrawMoneyCommand command){
apply(new MoneyWithdrawnEvent(command.getAccountId(), command.getAmount()));
}
@EventHandler
public void on(AccountCreatedEvent event){
this.accountId = event.getAccountId();
this.accountName = event.getAccountName();
this.balance = new BigDecimal(event.getAmount());
LOGGER.info("Account {} is created with balance {}", accountId, this.balance);
}
@EventHandler
public void on(MoneyWithdrawnEvent event){
BigDecimal result = this.balance.subtract(new BigDecimal(event.getAmount()));
if(result.compareTo(BigDecimal.ZERO)<0)
LOGGER.error("Cannot withdraw more money than the balance!");
else {
this.balance = result;
LOGGER.info("Withdraw {} from account {}, balance result: {}", event.getAmount(), accountId, balance);
}
}
}

View File

@@ -0,0 +1,29 @@
package com.edi.learn.axon.command.commands;
import com.edi.learn.axon.common.domain.AccountId;
public class CreateAccountCommand {
private AccountId accountId;
private String accountName;
private long amount;
public CreateAccountCommand(AccountId accountId, String accountName, long amount) {
this.accountId = accountId;
this.accountName = accountName;
this.amount = amount;
}
public AccountId getAccountId() {
return accountId;
}
public String getAccountName() {
return accountName;
}
public long getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,26 @@
package com.edi.learn.axon.command.commands;
import com.edi.learn.axon.common.domain.AccountId;
import org.axonframework.commandhandling.TargetAggregateIdentifier;
public class WithdrawMoneyCommand {
@TargetAggregateIdentifier
private AccountId accountId;
private long amount;
public WithdrawMoneyCommand(AccountId accountId, long amount) {
this.accountId = accountId;
this.amount = amount;
}
public AccountId getAccountId() {
return accountId;
}
public long getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,48 @@
package com.edi.learn.axon.common.domain;
import org.axonframework.common.Assert;
import org.axonframework.common.IdentifierFactory;
import java.io.Serializable;
public class AccountId implements Serializable {
private static final long serialVersionUID = 7119961474083133148L;
private final String identifier;
private final int hashCode;
public AccountId() {
this.identifier = IdentifierFactory.getInstance().generateIdentifier();
this.hashCode = identifier.hashCode();
}
public AccountId(String identifier) {
Assert.notNull(identifier, ()->"Identifier may not be null");
this.identifier = identifier;
this.hashCode = identifier.hashCode();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AccountId accountId = (AccountId) o;
return identifier.equals(accountId.identifier);
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public String toString() {
return identifier;
}
}

View File

@@ -0,0 +1,28 @@
package com.edi.learn.axon.common.events;
import com.edi.learn.axon.common.domain.AccountId;
public class AccountCreatedEvent {
private AccountId accountId;
private String accountName;
private long amount;
public AccountCreatedEvent(AccountId accountId, String accountName, long amount) {
this.accountId = accountId;
this.accountName = accountName;
this.amount = amount;
}
public AccountId getAccountId() {
return accountId;
}
public String getAccountName() {
return accountName;
}
public long getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,22 @@
package com.edi.learn.axon.common.events;
import com.edi.learn.axon.common.domain.AccountId;
public class MoneyWithdrawnEvent {
private AccountId accountId;
private long amount;
public MoneyWithdrawnEvent(AccountId accountId, long amount) {
this.accountId = accountId;
this.amount = amount;
}
public AccountId getAccountId() {
return accountId;
}
public long getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,14 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>

7
lesson-2/README.md Normal file
View File

@@ -0,0 +1,7 @@
Lesson-2
---
In this sample, we integrate SpringBoot and Axon framework to do the same thing with Lesson-1.
Meanwhile, we also learned how to use JPA repository to store the state of Aggregates, which is one of the two ways supported
by Axon,
- Standard Repository to store the state of Aggregate directly
- Event sourcing Repository to store the events ever happened and get the state by replaying all the events

34
lesson-2/pom.xml Normal file
View File

@@ -0,0 +1,34 @@
<?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">
<parent>
<artifactId>sbs-axon</artifactId>
<groupId>com.edi.learn</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>lesson-2</artifactId>
<version>2</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!--<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>-->
</dependencies>
</project>

View File

@@ -0,0 +1,21 @@
package com.edi.learn.axon;
import org.slf4j.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import static org.slf4j.LoggerFactory.getLogger;
@SpringBootApplication
@ComponentScan(basePackages = {"com.edi.learn"})
public class Application {
private static final Logger LOGGER = getLogger(Application.class);
public static void main(String args[]){
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,93 @@
package com.edi.learn.axon.command.aggregates;
import com.edi.learn.axon.command.commands.CreateAccountCommand;
import com.edi.learn.axon.command.commands.WithdrawMoneyCommand;
import com.edi.learn.axon.common.domain.AccountId;
import com.edi.learn.axon.common.events.AccountCreatedEvent;
import com.edi.learn.axon.common.events.MoneyWithdrawnEvent;
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.commandhandling.model.AggregateIdentifier;
import org.axonframework.eventhandling.EventHandler;
import org.axonframework.spring.stereotype.Aggregate;
import org.slf4j.Logger;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import java.math.BigDecimal;
import static org.axonframework.commandhandling.model.AggregateLifecycle.apply;
import static org.slf4j.LoggerFactory.getLogger;
@Aggregate(repository = "accountRepository")
@Entity
public class BankAccount {
private static final Logger LOGGER = getLogger(BankAccount.class);
@AggregateIdentifier
private AccountId accountId;
private String accountName;
private BigDecimal balance;
public BankAccount() {
}
@CommandHandler
public BankAccount(CreateAccountCommand command){
LOGGER.debug("Construct a new BankAccount");
apply(new AccountCreatedEvent(command.getAccountId(), command.getAccountName(), command.getAmount()));
}
@CommandHandler
public void handle(WithdrawMoneyCommand command){
apply(new MoneyWithdrawnEvent(command.getAccountId(), command.getAmount()));
}
@EventHandler
public void on(AccountCreatedEvent event){
this.accountId = event.getAccountId();
this.accountName = event.getAccountName();
this.balance = new BigDecimal(event.getAmount());
LOGGER.info("Account {} is created with balance {}", accountId, this.balance);
}
@EventHandler
public void on(MoneyWithdrawnEvent event){
BigDecimal result = this.balance.subtract(new BigDecimal(event.getAmount()));
if(result.compareTo(BigDecimal.ZERO)<0)
LOGGER.error("Cannot withdraw more money than the balance!");
else {
this.balance = result;
LOGGER.info("Withdraw {} from account {}, balance result: {}", event.getAmount(), accountId, balance);
}
}
@Id
public String getAccountId() {
return accountId.toString();
}
public void setAccountId(String accountId) {
this.accountId = new AccountId(accountId);
}
@Column
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
@Column
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
}

View File

@@ -0,0 +1,30 @@
package com.edi.learn.axon.command.commands;
import com.edi.learn.axon.common.domain.AccountId;
public class CreateAccountCommand {
private AccountId accountId;
private String accountName;
private long amount;
public CreateAccountCommand(AccountId accountId, String accountName, long amount) {
this.accountId = accountId;
this.accountName = accountName;
this.amount = amount;
}
public AccountId getAccountId() {
return accountId;
}
public String getAccountName() {
return accountName;
}
public long getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,26 @@
package com.edi.learn.axon.command.commands;
import com.edi.learn.axon.common.domain.AccountId;
import org.axonframework.commandhandling.TargetAggregateIdentifier;
public class WithdrawMoneyCommand {
@TargetAggregateIdentifier
private AccountId accountId;
private long amount;
public WithdrawMoneyCommand(AccountId accountId, long amount) {
this.accountId = accountId;
this.amount = amount;
}
public AccountId getAccountId() {
return accountId;
}
public long getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,43 @@
package com.edi.learn.axon.command.controller;
import com.edi.learn.axon.command.commands.CreateAccountCommand;
import com.edi.learn.axon.command.commands.WithdrawMoneyCommand;
import com.edi.learn.axon.common.domain.AccountId;
import org.axonframework.commandhandling.gateway.CommandGateway;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Created by Edison on 2017/3/9.
*/
@RestController
@RequestMapping("/bank")
public class BankAccountController {
private static final Logger LOGGER = getLogger(BankAccountController.class);
@Autowired
private CommandGateway commandGateway;
@Autowired
private HttpServletResponse response;
@RequestMapping(method = RequestMethod.POST)
public void create() {
LOGGER.info("start");
AccountId id = new AccountId();
LOGGER.debug("Account id: {}", id.toString());
commandGateway.send(new CreateAccountCommand(id, "MyAccount",1000));
commandGateway.send(new WithdrawMoneyCommand(id, 500));
commandGateway.send(new WithdrawMoneyCommand(id, 300));
commandGateway.send(new CreateAccountCommand(id, "MyAccount", 1000));
commandGateway.send(new WithdrawMoneyCommand(id, 500));
}
}

View File

@@ -0,0 +1,76 @@
package com.edi.learn.axon.common.config;
import com.edi.learn.axon.command.aggregates.BankAccount;
import org.axonframework.commandhandling.CommandBus;
import org.axonframework.commandhandling.SimpleCommandBus;
import org.axonframework.commandhandling.model.GenericJpaRepository;
import org.axonframework.commandhandling.model.Repository;
import org.axonframework.common.jpa.ContainerManagedEntityManagerProvider;
import org.axonframework.common.jpa.EntityManagerProvider;
import org.axonframework.common.transaction.TransactionManager;
import org.axonframework.eventhandling.EventBus;
import org.axonframework.eventhandling.SimpleEventBus;
import org.axonframework.eventsourcing.eventstore.EventStorageEngine;
import org.axonframework.eventsourcing.eventstore.inmemory.InMemoryEventStorageEngine;
import org.axonframework.messaging.interceptors.TransactionManagingInterceptor;
import org.axonframework.monitoring.NoOpMessageMonitor;
import org.axonframework.spring.config.EnableAxon;
import org.axonframework.spring.messaging.unitofwork.SpringTransactionManager;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Created by Edison on 2017/3/9.
*/
@Configuration
@EnableAxon
public class JpaConfig {
private static final Logger LOGGER = getLogger(JpaConfig.class);
@Autowired
private PlatformTransactionManager transactionManager;
@Bean
public EventStorageEngine eventStorageEngine(){
return new InMemoryEventStorageEngine();
}
@Bean
public TransactionManager axonTransactionManager() {
return new SpringTransactionManager(transactionManager);
}
@Bean
public EventBus eventBus(){
return new SimpleEventBus();
}
@Bean
public CommandBus commandBus() {
SimpleCommandBus commandBus = new SimpleCommandBus(axonTransactionManager(), NoOpMessageMonitor.INSTANCE);
//commandBus.registerHandlerInterceptor(transactionManagingInterceptor());
return commandBus;
}
@Bean
public TransactionManagingInterceptor transactionManagingInterceptor(){
return new TransactionManagingInterceptor(new SpringTransactionManager(transactionManager));
}
@Bean
public EntityManagerProvider entityManagerProvider() {
return new ContainerManagedEntityManagerProvider();
}
@Bean
public Repository<BankAccount> accountRepository(){
return new GenericJpaRepository<BankAccount>(entityManagerProvider(),BankAccount.class, eventBus());
}
}

View File

@@ -0,0 +1,48 @@
package com.edi.learn.axon.common.domain;
import org.axonframework.common.Assert;
import org.axonframework.common.IdentifierFactory;
import java.io.Serializable;
public class AccountId implements Serializable {
private static final long serialVersionUID = 7119961474083133148L;
private final String identifier;
private final int hashCode;
public AccountId() {
this.identifier = IdentifierFactory.getInstance().generateIdentifier();
this.hashCode = identifier.hashCode();
}
public AccountId(String identifier) {
Assert.notNull(identifier, ()->"Identifier may not be null");
this.identifier = identifier;
this.hashCode = identifier.hashCode();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AccountId accountId = (AccountId) o;
return identifier.equals(accountId.identifier);
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public String toString() {
return identifier;
}
}

View File

@@ -0,0 +1,29 @@
package com.edi.learn.axon.common.events;
import com.edi.learn.axon.common.domain.AccountId;
public class AccountCreatedEvent {
private AccountId accountId;
private String accountName;
private long amount;
public AccountCreatedEvent(AccountId accountId, String accountName, long amount) {
this.accountId = accountId;
this.accountName = accountName;
this.amount = amount;
}
public AccountId getAccountId() {
return accountId;
}
public String getAccountName() {
return accountName;
}
public long getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,23 @@
package com.edi.learn.axon.common.events;
import com.edi.learn.axon.common.domain.AccountId;
public class MoneyWithdrawnEvent {
private AccountId accountId;
private long amount;
public MoneyWithdrawnEvent(AccountId accountId, long amount) {
this.accountId = accountId;
this.amount = amount;
}
public AccountId getAccountId() {
return accountId;
}
public long getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,19 @@
# Datasource configuration
spring.datasource.url=jdbc:h2:mem:exploredb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
#spring.datasource.url=jdbc:mysql://localhost:3306/cqrs
#spring.datasource.driverClassName=com.mysql.jdbc.Driver
#spring.datasource.username=root
#spring.datasource.password=Welcome1
#spring.datasource.validation-query=SELECT 1;
#spring.datasource.initial-size=2
#spring.datasource.sql-script-encoding=UTF-8
spring.jpa.database=h2
#spring.jpa.database=mysql
#spring.jpa.show-sql=true
#spring.jpa.hibernate.ddl-auto=create-drop
spring.h2.console.enabled=true

View File

@@ -0,0 +1,17 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
<logger name="com.edi.learn" level="debug" />
<logger name="org.axonframework" level="info" />
</configuration>

9
lesson-3/README.md Normal file
View File

@@ -0,0 +1,9 @@
Lesson-3
---
As mentioned in Lesson-2, Axon framework supports two mechanisms to maintain the state of aggregates.
- Standard Repository to store the state of Aggregate directly
- Event sourcing Repository to store the events ever happened and get the state by replaying all the events
In this lesson, we implement the event sourcing repository, which is the default option when using axon-spring-boot-autoconfigure
module.
All the events will be stored in the MySql through JPA.

38
lesson-3/pom.xml Normal file
View File

@@ -0,0 +1,38 @@
<?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">
<parent>
<artifactId>sbs-axon</artifactId>
<groupId>com.edi.learn</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>lesson-3</artifactId>
<version>3</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring-boot-autoconfigure</artifactId>
</dependency>
<!--<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,62 @@
package com.edi.learn.axon.command.aggregates;
import com.edi.learn.axon.command.commands.CreateAccountCommand;
import com.edi.learn.axon.command.commands.WithdrawMoneyCommand;
import com.edi.learn.axon.common.domain.AccountId;
import com.edi.learn.axon.common.events.AccountCreatedEvent;
import com.edi.learn.axon.common.events.MoneyWithdrawnEvent;
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.commandhandling.model.AggregateIdentifier;
import org.axonframework.eventhandling.EventHandler;
import org.axonframework.spring.stereotype.Aggregate;
import org.slf4j.Logger;
import java.math.BigDecimal;
import static org.axonframework.commandhandling.model.AggregateLifecycle.apply;
import static org.slf4j.LoggerFactory.getLogger;
@Aggregate
public class BankAccount {
private static final Logger LOGGER = getLogger(BankAccount.class);
@AggregateIdentifier
private AccountId accountId;
private String accountName;
private BigDecimal balance;
public BankAccount() {
}
@CommandHandler
public BankAccount(CreateAccountCommand command){
LOGGER.debug("Construct a new BankAccount");
apply(new AccountCreatedEvent(command.getAccountId(), command.getAccountName(), command.getAmount()));
}
@CommandHandler
public void handle(WithdrawMoneyCommand command){
apply(new MoneyWithdrawnEvent(command.getAccountId(), command.getAmount()));
}
@EventHandler
public void on(AccountCreatedEvent event){
this.accountId = event.getAccountId();
this.accountName = event.getAccountName();
this.balance = new BigDecimal(event.getAmount());
LOGGER.info("Account {} is created with balance {}", accountId, this.balance);
}
@EventHandler
public void on(MoneyWithdrawnEvent event){
BigDecimal result = this.balance.subtract(new BigDecimal(event.getAmount()));
if(result.compareTo(BigDecimal.ZERO)<0)
LOGGER.error("Cannot withdraw more money than the balance!");
else {
this.balance = result;
LOGGER.info("Withdraw {} from account {}, balance result: {}", event.getAmount(), accountId, balance);
}
}
}

View File

@@ -0,0 +1,30 @@
package com.edi.learn.axon.command.commands;
import com.edi.learn.axon.common.domain.AccountId;
public class CreateAccountCommand {
private AccountId accountId;
private String accountName;
private long amount;
public CreateAccountCommand(AccountId accountId, String accountName, long amount) {
this.accountId = accountId;
this.accountName = accountName;
this.amount = amount;
}
public AccountId getAccountId() {
return accountId;
}
public String getAccountName() {
return accountName;
}
public long getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,26 @@
package com.edi.learn.axon.command.commands;
import com.edi.learn.axon.common.domain.AccountId;
import org.axonframework.commandhandling.TargetAggregateIdentifier;
public class WithdrawMoneyCommand {
@TargetAggregateIdentifier
private AccountId accountId;
private long amount;
public WithdrawMoneyCommand(AccountId accountId, long amount) {
this.accountId = accountId;
this.amount = amount;
}
public AccountId getAccountId() {
return accountId;
}
public long getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,48 @@
package com.edi.learn.axon.common.domain;
import org.axonframework.common.Assert;
import org.axonframework.common.IdentifierFactory;
import java.io.Serializable;
public class AccountId implements Serializable {
private static final long serialVersionUID = 7119961474083133148L;
private final String identifier;
private final int hashCode;
public AccountId() {
this.identifier = IdentifierFactory.getInstance().generateIdentifier();
this.hashCode = identifier.hashCode();
}
public AccountId(String identifier) {
Assert.notNull(identifier, ()->"Identifier may not be null");
this.identifier = identifier;
this.hashCode = identifier.hashCode();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AccountId accountId = (AccountId) o;
return identifier.equals(accountId.identifier);
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public String toString() {
return identifier;
}
}

View File

@@ -0,0 +1,29 @@
package com.edi.learn.axon.common.events;
import com.edi.learn.axon.common.domain.AccountId;
public class AccountCreatedEvent {
private AccountId accountId;
private String accountName;
private long amount;
public AccountCreatedEvent(AccountId accountId, String accountName, long amount) {
this.accountId = accountId;
this.accountName = accountName;
this.amount = amount;
}
public AccountId getAccountId() {
return accountId;
}
public String getAccountName() {
return accountName;
}
public long getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,23 @@
package com.edi.learn.axon.common.events;
import com.edi.learn.axon.common.domain.AccountId;
public class MoneyWithdrawnEvent {
private AccountId accountId;
private long amount;
public MoneyWithdrawnEvent(AccountId accountId, long amount) {
this.accountId = accountId;
this.amount = amount;
}
public AccountId getAccountId() {
return accountId;
}
public long getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,43 @@
package com.edi.learn.axon.controller;
import com.edi.learn.axon.command.commands.CreateAccountCommand;
import com.edi.learn.axon.command.commands.WithdrawMoneyCommand;
import com.edi.learn.axon.common.domain.AccountId;
import org.axonframework.commandhandling.gateway.CommandGateway;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Created by Edison on 2017/3/9.
*/
@RestController
@RequestMapping("/bank")
public class BankAccountController {
private static final Logger LOGGER = getLogger(BankAccountController.class);
@Autowired
private CommandGateway commandGateway;
@Autowired
private HttpServletResponse response;
@RequestMapping(method = RequestMethod.POST)
public void create() {
LOGGER.info("start");
AccountId id = new AccountId();
LOGGER.debug("Account id: {}", id.toString());
commandGateway.send(new CreateAccountCommand(id, "MyAccount",1000));
commandGateway.send(new WithdrawMoneyCommand(id, 500));
commandGateway.send(new WithdrawMoneyCommand(id, 300));
/*config.commandBus().dispatch(asCommandMessage(new CreateAccountCommand(id, "MyAccount", 1000)));
config.commandBus().dispatch(asCommandMessage(new WithdrawMoneyCommand(id, 500)));*/
}
}

View File

@@ -0,0 +1,22 @@
package com.edi.learn.axon.main;
import org.slf4j.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import static org.slf4j.LoggerFactory.getLogger;
@SpringBootApplication
@ComponentScan(basePackages = {"com.edi.learn"})
public class Application {
private static final Logger LOGGER = getLogger(Application.class);
public static void main(String args[]){
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,19 @@
# Datasource configuration
# spring.datasource.url=jdbc:h2:mem:exploredb
# spring.datasource.driverClassName=org.h2.Driver
# spring.datasource.username=sa
# spring.datasource.password=
spring.datasource.url=jdbc:mysql://localhost:3306/cqrs
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=Welcome1
spring.datasource.validation-query=SELECT 1;
spring.datasource.initial-size=2
spring.datasource.sql-script-encoding=UTF-8
# spring.jpa.database=h2
spring.jpa.database=mysql
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update

View File

@@ -0,0 +1,17 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
<logger name="com.edi.learn" level="debug" />
<logger name="org.axonframework" level="info" />
</configuration>

11
lesson-4/README.md Normal file
View File

@@ -0,0 +1,11 @@
Lesson-4
---
From this lesson, we will try to use Axon to implement a more complex case - the online shop.
We'll climb the mountain step by step.
In this lesson, we simply learn the following things.
- How to use MongoDB as the event store
- How to use MySql as the query database
- In the CQRS, how to separate the command & query
Note:
To avoid losing accuracy caused by using `float/double`, I multiply 100 with the price, and use `long` to store the price.

52
lesson-4/pom.xml Normal file
View File

@@ -0,0 +1,52 @@
<?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">
<parent>
<artifactId>sbs-axon</artifactId>
<groupId>com.edi.learn</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>lesson-4</artifactId>
<version>4</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-mongo</artifactId>
<version>${axon.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.6.5</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,77 @@
package com.edi.learn.axon.command.aggregates;
import com.edi.learn.axon.common.domain.OrderId;
import com.edi.learn.axon.common.domain.OrderProduct;
import com.edi.learn.axon.common.events.OrderCreatedEvent;
import org.axonframework.commandhandling.model.AggregateIdentifier;
import org.axonframework.commandhandling.model.AggregateMember;
import org.axonframework.eventhandling.EventHandler;
import org.axonframework.spring.stereotype.Aggregate;
import java.util.Map;
import static org.axonframework.commandhandling.model.AggregateLifecycle.apply;
@Aggregate
public class OrderAggregate {
@AggregateIdentifier
private OrderId id;
private String username;
private double payment;
@AggregateMember
private Map<String, OrderProduct> products;
public OrderAggregate(){}
public OrderAggregate(OrderId id, String username, Map<String, OrderProduct> products) {
apply(new OrderCreatedEvent(id, username, products));
}
public OrderId getId() {
return id;
}
public String getUsername() {
return username;
}
public Map<String, OrderProduct> getProducts() {
return products;
}
@EventHandler
public void on(OrderCreatedEvent event){
this.id = event.getOrderId();
this.username = event.getUsername();
this.products = event.getProducts();
computePrice();
}
private void computePrice() {
products.forEach((id, product) -> {
payment += product.getPrice() * product.getAmount();
});
}
/**
* Divided 100 here because of the transformation of accuracy
*
* @return
*/
public double getPayment() {
return payment/100;
}
public void addProduct(OrderProduct product){
this.products.put(product.getId(), product);
payment += product.getPrice() * product.getAmount();
}
public void removeProduct(String productId){
OrderProduct product = this.products.remove(productId);
payment = payment - product.getPrice() * product.getAmount();
}
}

View File

@@ -0,0 +1,54 @@
package com.edi.learn.axon.command.aggregates;
import com.edi.learn.axon.command.commands.CreateProductCommand;
import com.edi.learn.axon.common.events.ProductCreatedEvent;
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.commandhandling.model.AggregateIdentifier;
import org.axonframework.eventhandling.EventHandler;
import org.axonframework.spring.stereotype.Aggregate;
import org.slf4j.Logger;
import static org.axonframework.commandhandling.model.AggregateLifecycle.apply;
import static org.slf4j.LoggerFactory.getLogger;
@Aggregate
public class ProductAggregate {
private static final Logger LOGGER = getLogger(ProductAggregate.class);
@AggregateIdentifier
private String id;
private String name;
private int stock;
private long price;
public ProductAggregate() {
}
@CommandHandler
public ProductAggregate(CreateProductCommand command) {
apply(new ProductCreatedEvent(command.getId(),command.getName(),command.getPrice(),command.getStock()));
}
@EventHandler
public void on(ProductCreatedEvent event){
this.id = event.getId();
this.name = event.getName();
this.price = event.getPrice();
this.stock = event.getStock();
LOGGER.debug("Product [{}] {} {}x{} is created.", id,name,price,stock);
}
public String getName() {
return name;
}
public int getStock() {
return stock;
}
public long getPrice() {
return price;
}
}

View File

@@ -0,0 +1,31 @@
package com.edi.learn.axon.command.commands;
import com.edi.learn.axon.common.domain.OrderId;
import java.util.Map;
public class CreateOrderCommand {
private OrderId orderId;
private String username;
private Map<String, Integer> products;
public CreateOrderCommand(String username, Map<String, Integer> products) {
this.orderId = new OrderId();
this.username = username;
this.products = products;
}
public OrderId getOrderId() {
return orderId;
}
public String getUsername() {
return username;
}
public Map<String, Integer> getProducts() {
return products;
}
}

View File

@@ -0,0 +1,36 @@
package com.edi.learn.axon.command.commands;
public class CreateProductCommand {
//@TargetAggregateIdentifier
// here @TargetAggregateIdentifier annotation is optional because it's a construct command
private String id;
private String name;
private long price;
private int stock;
public CreateProductCommand(String id, String name, long price, int stock) {
this.id = id;
this.name = name;
this.price = price;
this.stock = stock;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public long getPrice() {
return price;
}
public int getStock() {
return stock;
}
}

View File

@@ -0,0 +1,26 @@
package com.edi.learn.axon.command.commands;
import org.axonframework.commandhandling.TargetAggregateIdentifier;
/**
* Created by Edison Xu on 2017/3/9.
*/
public class ReserveStockCommand {
@TargetAggregateIdentifier
private String productId;
private long number;
public ReserveStockCommand(String productId, long number) {
this.productId = productId;
this.number = number;
}
public String getProductId() {
return productId;
}
public long getNumber() {
return number;
}
}

View File

@@ -0,0 +1,44 @@
package com.edi.learn.axon.command.config;
import com.edi.learn.axon.command.aggregates.OrderAggregate;
import org.axonframework.commandhandling.model.Repository;
import org.axonframework.eventsourcing.AggregateFactory;
import org.axonframework.eventsourcing.EventSourcingRepository;
import org.axonframework.eventsourcing.eventstore.EventStore;
import org.axonframework.spring.eventsourcing.SpringPrototypeAggregateFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
/**
* Created by Edison Xu on 2017/3/14.
*/
@Configuration
public class OrderConfig {
@Autowired
private EventStore eventStore;
@Bean
@Scope("prototype")
public OrderAggregate orderAggregate(){
return new OrderAggregate();
}
@Bean
public AggregateFactory<OrderAggregate> orderAggregateAggregateFactory(){
SpringPrototypeAggregateFactory<OrderAggregate> aggregateFactory = new SpringPrototypeAggregateFactory<>();
aggregateFactory.setPrototypeBeanName("orderAggregate");
return aggregateFactory;
}
@Bean
public Repository<OrderAggregate> orderAggregateRepository(){
EventSourcingRepository<OrderAggregate> repository = new EventSourcingRepository<OrderAggregate>(
orderAggregateAggregateFactory(),
eventStore
);
return repository;
}
}

View File

@@ -0,0 +1,44 @@
package com.edi.learn.axon.command.config;
import com.edi.learn.axon.command.aggregates.ProductAggregate;
import org.axonframework.commandhandling.model.Repository;
import org.axonframework.eventsourcing.AggregateFactory;
import org.axonframework.eventsourcing.EventSourcingRepository;
import org.axonframework.eventsourcing.eventstore.EventStore;
import org.axonframework.spring.eventsourcing.SpringPrototypeAggregateFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
/**
* Created by Edison Xu on 2017/3/14.
*/
@Configuration
public class ProductConfig {
@Autowired
private EventStore eventStore;
@Bean
@Scope("prototype")
public ProductAggregate productAggregate(){
return new ProductAggregate();
}
@Bean
public AggregateFactory<ProductAggregate> productAggregateAggregateFactory(){
SpringPrototypeAggregateFactory<ProductAggregate> aggregateFactory = new SpringPrototypeAggregateFactory<>();
aggregateFactory.setPrototypeBeanName("productAggregate");
return aggregateFactory;
}
@Bean
public Repository<ProductAggregate> productAggregateRepository(){
EventSourcingRepository<ProductAggregate> repository = new EventSourcingRepository<ProductAggregate>(
productAggregateAggregateFactory(),
eventStore
);
return repository;
}
}

View File

@@ -0,0 +1,51 @@
package com.edi.learn.axon.command.handlers;
import com.edi.learn.axon.command.aggregates.OrderAggregate;
import com.edi.learn.axon.command.aggregates.ProductAggregate;
import com.edi.learn.axon.command.commands.CreateOrderCommand;
import com.edi.learn.axon.common.domain.OrderProduct;
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.commandhandling.model.Aggregate;
import org.axonframework.commandhandling.model.Repository;
import org.axonframework.eventhandling.EventBus;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Created by Edison on 2017/3/9.
*/
@Component
public class OrderHandler {
private static final Logger LOGGER = getLogger(OrderHandler.class);
@Autowired
private Repository<OrderAggregate> repository;
@Autowired
private Repository<ProductAggregate> productRepository;
@Autowired
private EventBus eventBus;
@CommandHandler
public void handle(CreateOrderCommand command) throws Exception {
Map<String, OrderProduct> products = new HashMap<>();
command.getProducts().forEach((productId,number)->{
LOGGER.debug("Loading product information with productId: {}",productId);
Aggregate<ProductAggregate> aggregate = productRepository.load(productId);
products.put(productId,
new OrderProduct(productId,
aggregate.invoke(productAggregate -> productAggregate.getName()),
aggregate.invoke(productAggregate -> productAggregate.getPrice()),
number));
});
repository.newInstance(() -> new OrderAggregate(command.getOrderId(), command.getUsername(), products));
}
}

View File

@@ -0,0 +1,61 @@
package com.edi.learn.axon.command.web.controllers;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.edi.learn.axon.command.commands.CreateOrderCommand;
import org.axonframework.commandhandling.gateway.CommandGateway;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import static org.slf4j.LoggerFactory.getLogger;
@RestController
@RequestMapping("/order")
public class OrderController {
private static final Logger LOGGER = getLogger(OrderController.class);
@Autowired
private CommandGateway commandGateway;
@Autowired
private HttpServletResponse response;
@RequestMapping(method = RequestMethod.POST)
public void create(@RequestBody(required = true) JSONObject input){
LOGGER.info(input.toJSONString());
int responseCode = HttpServletResponse.SC_BAD_REQUEST;
if(input.containsKey("username") && input.containsKey("products")){
String username = input.getString("username");
JSONArray products = input.getJSONArray("products");
if(!StringUtils.isEmpty(username) && products.size()>0){
Map<String, Integer> map = new HashMap<>();
CreateOrderCommand command = new CreateOrderCommand(username, map);
for(Object each:products){
JSONObject o = (JSONObject)each;
if(!o.containsKey("id") || !o.containsKey("number"))
return;
map.put(o.getString("id"), o.getInteger("number"));
}
commandGateway.sendAndWait(command);
responseCode = HttpServletResponse.SC_CREATED;
}
}
response.setStatus(responseCode);
}
}

View File

@@ -0,0 +1,53 @@
package com.edi.learn.axon.command.web.controllers;
import com.edi.learn.axon.command.commands.CreateProductCommand;
import org.axonframework.commandhandling.CommandExecutionException;
import org.axonframework.commandhandling.gateway.CommandGateway;
import org.axonframework.commandhandling.model.ConcurrencyException;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import static org.slf4j.LoggerFactory.getLogger;
@RestController
@RequestMapping("/product")
public class ProductController {
private static final Logger LOGGER = getLogger(ProductController.class);
@Autowired
private CommandGateway commandGateway;
@RequestMapping(value = "/{id}", method = RequestMethod.POST)
public void create(@PathVariable(value = "id") String id,
@RequestParam(value = "name", required = true) String name,
@RequestParam(value = "price", required = true) long price,
@RequestParam(value = "stock",required = true) int stock,
HttpServletResponse response) {
LOGGER.debug("Adding Product [{}] '{}' {}x{}", id, name, price, stock);
try {
// multiply 100 on the price to avoid float number
CreateProductCommand command = new CreateProductCommand(id,name,price*100,stock);
commandGateway.sendAndWait(command);
response.setStatus(HttpServletResponse.SC_CREATED);// Set up the 201 CREATED response
return;
} catch (CommandExecutionException cex) {
LOGGER.warn("Add Command FAILED with Message: {}", cex.getMessage());
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
if (null != cex.getCause()) {
LOGGER.warn("Caused by: {} {}", cex.getCause().getClass().getName(), cex.getCause().getMessage());
if (cex.getCause() instanceof ConcurrencyException) {
LOGGER.warn("A duplicate product with the same ID [{}] already exists.", id);
response.setStatus(HttpServletResponse.SC_CONFLICT);
}
}
}
}
}

View File

@@ -0,0 +1,65 @@
package com.edi.learn.axon.common.config;
import com.mongodb.MongoClient;
import com.mongodb.ServerAddress;
import org.axonframework.eventsourcing.eventstore.EventStorageEngine;
import org.axonframework.mongo.eventsourcing.eventstore.DefaultMongoTemplate;
import org.axonframework.mongo.eventsourcing.eventstore.MongoEventStorageEngine;
import org.axonframework.mongo.eventsourcing.eventstore.MongoFactory;
import org.axonframework.mongo.eventsourcing.eventstore.MongoTemplate;
import org.axonframework.mongo.eventsourcing.eventstore.documentperevent.DocumentPerEventStorageStrategy;
import org.axonframework.serialization.Serializer;
import org.axonframework.serialization.json.JacksonSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
@Configuration
public class CommandRepositoryConfiguration {
@Value("${mongodb.url}")
private String mongoUrl;
@Value("${mongodb.dbname}")
private String mongoDbName;
@Value("${mongodb.events.collection.name}")
private String eventsCollectionName;
@Value("${mongodb.events.snapshot.collection.name}")
private String snapshotCollectionName;
@Bean
public Serializer axonJsonSerializer() {
return new JacksonSerializer();
}
@Bean
public EventStorageEngine eventStorageEngine(){
return new MongoEventStorageEngine(
axonJsonSerializer(),null, axonMongoTemplate(), new DocumentPerEventStorageStrategy());
}
@Bean(name = "axonMongoTemplate")
public MongoTemplate axonMongoTemplate() {
MongoTemplate template = new DefaultMongoTemplate(mongoClient(), mongoDbName, eventsCollectionName, snapshotCollectionName);
return template;
}
@Bean
public MongoClient mongoClient(){
MongoFactory mongoFactory = new MongoFactory();
mongoFactory.setMongoAddresses(Arrays.asList(new ServerAddress(mongoUrl)));
return mongoFactory.createMongo();
}
/*@Bean
public SagaStore sagaStore(){
org.axonframework.mongo.eventhandling.saga.repository.MongoTemplate mongoTemplate =
new org.axonframework.mongo.eventhandling.saga.repository.DefaultMongoTemplate(mongoClient());
return new MongoSagaStore(mongoTemplate, axonJsonSerializer());
}*/
}

View File

@@ -0,0 +1,50 @@
package com.edi.learn.axon.common.domain;
import org.axonframework.common.Assert;
import org.axonframework.common.IdentifierFactory;
import java.io.Serializable;
public class OrderId implements Serializable {
private static final long serialVersionUID = -4163440749566043686L;
private final String identifier;
private final int hashCode;
public OrderId() {
this.identifier = IdentifierFactory.getInstance().generateIdentifier();
this.hashCode = identifier.hashCode();
}
public OrderId(String identifier) {
Assert.notNull(identifier, ()->"Identifier may not be null");
this.identifier = identifier;
this.hashCode = identifier.hashCode();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof OrderId)) return false;
OrderId orderId = (OrderId) o;
return identifier.equals(orderId.identifier);
}
@Override
public int hashCode() {
return identifier.hashCode();
}
@Override
public String toString() {
return this.identifier;
}
public String getIdentifier() {
return identifier;
}
}

View File

@@ -0,0 +1,34 @@
package com.edi.learn.axon.common.domain;
/**
* Created by Edison on 2017/3/9.
*/
public class OrderProduct {
private String id;
private String name;
private long price;
private int amount;
public OrderProduct(String id, String name, long price, int amount) {
this.id = id;
this.name = name;
this.price = price;
this.amount = amount;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public long getPrice() {
return price;
}
public int getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,37 @@
package com.edi.learn.axon.common.events;
import com.edi.learn.axon.common.domain.OrderId;
import com.edi.learn.axon.common.domain.OrderProduct;
import org.axonframework.commandhandling.TargetAggregateIdentifier;
import java.util.Map;
public class OrderCreatedEvent {
@TargetAggregateIdentifier
private OrderId orderId;
private String username;
private Map<String, OrderProduct> products;
public OrderCreatedEvent() {
}
public OrderCreatedEvent(OrderId orderId, String username, Map<String, OrderProduct> products) {
this.orderId = orderId;
this.username = username;
this.products = products;
}
public OrderId getOrderId() {
return orderId;
}
public String getUsername() {
return username;
}
public Map<String, OrderProduct> getProducts() {
return products;
}
}

View File

@@ -0,0 +1,40 @@
package com.edi.learn.axon.common.events;
public class ProductCreatedEvent {
private String id;
private String name;
private long price;
private int stock;
/**
* If no empty args constructor, jackson will throw serialization exceptions
*/
public ProductCreatedEvent() {
System.out.println("Product event created");
}
public ProductCreatedEvent(String id, String name, long price, int stock) {
this.id = id;
this.name = name;
this.price = price;
this.stock = stock;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public long getPrice() {
return price;
}
public int getStock() {
return stock;
}
}

View File

@@ -0,0 +1,32 @@
package com.edi.learn.axon.common.util;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter4;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class FastJsonConverter {
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter4 fastConverter = new FastJsonHttpMessageConverter4();
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.add(MediaType.APPLICATION_JSON);
supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
fastConverter.setSupportedMediaTypes(supportedMediaTypes);
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastConverter.setFastJsonConfig(fastJsonConfig);
HttpMessageConverter<?> converter = fastConverter;
return new HttpMessageConverters(converter);
}
}

View File

@@ -0,0 +1,27 @@
package com.edi.learn.axon.main;
import org.slf4j.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import static org.slf4j.LoggerFactory.getLogger;
@SpringBootApplication
@ComponentScan(basePackages = {"com.edi.learn"})
@EntityScan(basePackages = {"com.edi.learn",
"org.axonframework.eventsourcing.eventstore.jpa",
"org.axonframework.eventhandling.saga.repository.jpa",
"org.axonframework.eventhandling.tokenstore.jpa"})
@EnableJpaRepositories(basePackages = {"com.edi.learn.axon.query"})
public class Application {
private static final Logger LOGGER = getLogger(Application.class);
public static void main(String args[]){
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,63 @@
package com.edi.learn.axon.query.entries;
import javax.persistence.*;
import java.util.Map;
/**
* Created by Edison Xu on 2017/3/15.
*/
@Entity
public class OrderEntry {
@Id
private String id;
@Column
private String username;
@Column
private double payment;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
@MapKey(name = "id")
private Map<String, OrderProductEntry> products;
public OrderEntry() {
}
public OrderEntry(String id, String username, Map<String, OrderProductEntry> products) {
this.id = id;
this.username = username;
this.payment = payment;
this.products = products;
}
public String getId() {
return id.toString();
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public double getPayment() {
return payment;
}
public void setPayment(double payment) {
this.payment = payment;
}
public Map<String, OrderProductEntry> getProducts() {
return products;
}
public void setProducts(Map<String, OrderProductEntry> products) {
this.products = products;
}
}

View File

@@ -0,0 +1,68 @@
package com.edi.learn.axon.query.entries;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* Created by Edison Xu on 2017/3/15.
*/
@Entity
public class OrderProductEntry {
@Id
@GeneratedValue
private Long jpaId;
private String id;
@Column
private String name;
@Column
private long price;
@Column
private int amount;
public OrderProductEntry() {
}
public OrderProductEntry(String id, String name, long price, int amount) {
this.id = id;
this.name = name;
this.price = price;
this.amount = amount;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Long getJpaId() {
return jpaId;
}
public void setJpaId(Long jpaId) {
this.jpaId = jpaId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getPrice() {
return price;
}
public void setPrice(long price) {
this.price = price;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
}

View File

@@ -0,0 +1,67 @@
package com.edi.learn.axon.query.entries;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
/**
* Remember to add {@code EntityScan }annotation in your start application.
* <br>
* Meanwhile, you need to add some packages in Axon Framework to make it work.
* <br>
* Created by Edison Xu on 2017/3/14.
*/
@Entity
public class ProductEntry {
@Id
private String id;
@Column
private String name;
@Column
private long price;
@Column
private int stock;
public ProductEntry() {
}
public ProductEntry(String id, String name, long price, int stock) {
this.id = id;
this.name = name;
this.price = price;
this.stock = stock;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getStock() {
return stock;
}
public void setStock(int stock) {
this.stock = stock;
}
public int getPrice() {
return (int)price;
}
public void setPrice(long price) {
this.price = price;
}
}

View File

@@ -0,0 +1,44 @@
package com.edi.learn.axon.query.handlers;
import com.edi.learn.axon.common.events.OrderCreatedEvent;
import com.edi.learn.axon.query.entries.OrderEntry;
import com.edi.learn.axon.query.entries.OrderProductEntry;
import com.edi.learn.axon.query.repository.OrderEntryRepository;
import org.axonframework.eventhandling.EventHandler;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import static org.slf4j.LoggerFactory.getLogger;
/**
* This event handler is used to update the repository data of the query side in the CQRS.
* Created by Edison Xu on 2017/3/15.
*/
@Component
public class OrderEventHandler {
private static final Logger LOGGER = getLogger(OrderEventHandler.class);
@Autowired
private OrderEntryRepository repository;
@EventHandler
public void on(OrderCreatedEvent event){
Map<String, OrderProductEntry> map = new HashMap<>();
event.getProducts().forEach((id, product)->{
map.put(id,
new OrderProductEntry(
product.getId(),
product.getName(),
product.getPrice(),
product.getAmount()));
});
OrderEntry order = new OrderEntry(event.getOrderId().toString(), event.getUsername(), map);
repository.save(order);
}
}

View File

@@ -0,0 +1,31 @@
package com.edi.learn.axon.query.handlers;
import com.edi.learn.axon.common.events.ProductCreatedEvent;
import com.edi.learn.axon.query.entries.ProductEntry;
import com.edi.learn.axon.query.repository.ProductEntryRepository;
import org.axonframework.eventhandling.EventHandler;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import static org.slf4j.LoggerFactory.getLogger;
/**
* This event handler is used to update the repository data of the query side in the CQRS.
* Created by Edison Xu on 2017/3/7.
*/
@Component
public class ProductEventHandler {
private static final Logger LOGGER = getLogger(ProductEventHandler.class);
@Autowired
ProductEntryRepository repository;
@EventHandler
public void on(ProductCreatedEvent event){
// update the data in the cache or db of the query side
LOGGER.debug("repository data is updated");
repository.save(new ProductEntry(event.getId(), event.getName(), event.getPrice(), event.getStock()));
}
}

View File

@@ -0,0 +1,12 @@
package com.edi.learn.axon.query.repository;
import com.edi.learn.axon.query.entries.OrderEntry;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
/**
* Created by Edison Xu on 2017/3/15.
*/
@RepositoryRestResource(collectionResourceRel = "orders", path = "orders")
public interface OrderEntryRepository extends PagingAndSortingRepository<OrderEntry, String> {
}

View File

@@ -0,0 +1,15 @@
package com.edi.learn.axon.query.repository;
import com.edi.learn.axon.query.entries.ProductEntry;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
/**
* Remember to add {@code EnableJpaRepositories} in the start Application
* <br>
* Created by Edison Xu on 2017/3/14.
*/
@RepositoryRestResource(collectionResourceRel = "products", path = "products")
public interface ProductEntryRepository extends PagingAndSortingRepository<ProductEntry, String> {
}

View File

@@ -0,0 +1,28 @@
# Datasource configuration
# spring.datasource.url=jdbc:h2:mem:exploredb
# spring.datasource.driverClassName=org.h2.Driver
# spring.datasource.username=sa
# spring.datasource.password=
spring.datasource.url=jdbc:mysql://localhost:3306/productorder
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=Welcome1
spring.datasource.validation-query=SELECT 1;
spring.datasource.initial-size=2
spring.datasource.sql-script-encoding=UTF-8
# spring.jpa.database=h2
spring.jpa.database=mysql
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create-drop
# mongo
mongodb.url=localhost
mongodb.port=27017
# mongodb.username=
# mongodb.password=
mongodb.dbname=axon
mongodb.events.collection.name=events
mongodb.events.snapshot.collection.name=snapshots

View File

@@ -0,0 +1,17 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
<logger name="com.edi.learn" level="debug" />
<logger name="org.axonframework" level="info" />
</configuration>

131
pom.xml Normal file
View File

@@ -0,0 +1,131 @@
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
</parent>
<groupId>com.edi.learn</groupId>
<artifactId>sbs-axon</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>lesson-1</module>
<module>lesson-2</module>
<module>lesson-3</module>
<module>lesson-4</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<axon.version>3.0.4</axon.version>
<slf4j.version>1.7.23</slf4j.version>
<logback.version>1.2.1</logback.version>
<springframework.version>4.3.4.RELEASE</springframework.version>
<!-- Maven plugins -->
<maven.compiler.plugin>3.1</maven.compiler.plugin>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-core</artifactId>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-core</artifactId>
<version>${axon.version}</version>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring</artifactId>
<version>${axon.version}</version>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring-boot-autoconfigure</artifactId>
<version>${axon.version}</version>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-amqp</artifactId>
<version>${axon.version}</version>
</dependency>
<!--<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-mongo</artifactId>
<version>${axon.version}</version>
</dependency>-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.plugin}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>

7
test.json Normal file
View File

@@ -0,0 +1,7 @@
{
"username":"Daniel",
"products":[{
"id":1,
"number":90
}]
}