mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-05-14 10:58:42 +00:00
refactor: Event sourcing 2445 (#2518)
* #2445 Refactored event sourcing * #2445 Minor cleanup * #2445 Formatted with Google java style guide * #2445 Fixed Javadoc * #2445 Fixing sonar gate bug
This commit is contained in:
@@ -40,8 +40,8 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.iluwatar.event.sourcing.event.AccountCreateEvent;
|
||||
import com.iluwatar.event.sourcing.event.MoneyDepositEvent;
|
||||
import com.iluwatar.event.sourcing.event.MoneyTransferEvent;
|
||||
import com.iluwatar.event.sourcing.processor.DomainEventProcessor;
|
||||
import com.iluwatar.event.sourcing.processor.JsonFileJournal;
|
||||
import com.iluwatar.event.sourcing.state.AccountAggregate;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
@@ -42,16 +43,18 @@ import lombok.extern.slf4j.Slf4j;
|
||||
* transactional data, and maintain full audit trails and history that can enable compensating
|
||||
* actions.
|
||||
*
|
||||
* <p>This App class is an example usage of an Event Sourcing pattern. As an example, two bank accounts
|
||||
* are created, then some money deposit and transfer actions are taken, so a new state of accounts is
|
||||
* created. At that point, state is cleared in order to represent a system shut-down. After the shut-down,
|
||||
* system state is recovered by re-creating the past events from event journals. Then state is
|
||||
* printed so a user can view the last state is same with the state before a system shut-down.
|
||||
* <p>This App class is an example usage of an Event Sourcing pattern. As an example, two bank
|
||||
* accounts are created, then some money deposit and transfer actions are taken, so a new state of
|
||||
* accounts is created. At that point, state is cleared in order to represent a system shut-down.
|
||||
* After the shut-down, system state is recovered by re-creating the past events from event
|
||||
* journals. Then state is printed so a user can view the last state is same with the state before a
|
||||
* system shut-down.
|
||||
*
|
||||
* <p>Created by Serdar Hamzaogullari on 06.08.2017.
|
||||
*/
|
||||
@Slf4j
|
||||
public class App {
|
||||
|
||||
/**
|
||||
* The constant ACCOUNT OF DAENERYS.
|
||||
*/
|
||||
@@ -68,8 +71,7 @@ public class App {
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
|
||||
var eventProcessor = new DomainEventProcessor();
|
||||
|
||||
var eventProcessor = new DomainEventProcessor(new JsonFileJournal());
|
||||
|
||||
LOGGER.info("Running the system first time............");
|
||||
eventProcessor.reset();
|
||||
@@ -103,7 +105,7 @@ public class App {
|
||||
|
||||
LOGGER.info("Recover the system by the events in journal file............");
|
||||
|
||||
eventProcessor = new DomainEventProcessor();
|
||||
eventProcessor = new DomainEventProcessor(new JsonFileJournal());
|
||||
eventProcessor.recover();
|
||||
|
||||
LOGGER.info("...............Recovered State:............");
|
||||
|
||||
+8
-4
@@ -24,14 +24,15 @@
|
||||
*/
|
||||
package com.iluwatar.event.sourcing.event;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.iluwatar.event.sourcing.domain.Account;
|
||||
import com.iluwatar.event.sourcing.state.AccountAggregate;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* This is the class that implements account created event.
|
||||
* Holds the necessary info for an account created event.
|
||||
* Implements the process function that finds the event-related domain objects and
|
||||
* This is the class that implements account created event. Holds the necessary info for an account
|
||||
* created event. Implements the process function that finds the event-related domain objects and
|
||||
* calls the related domain object's handle event functions
|
||||
*
|
||||
* <p>Created by Serdar Hamzaogullari on 06.08.2017.
|
||||
@@ -50,7 +51,10 @@ public class AccountCreateEvent extends DomainEvent {
|
||||
* @param accountNo the account no
|
||||
* @param owner the owner
|
||||
*/
|
||||
public AccountCreateEvent(long sequenceId, long createdTime, int accountNo, String owner) {
|
||||
@JsonCreator
|
||||
public AccountCreateEvent(@JsonProperty("sequenceId") long sequenceId,
|
||||
@JsonProperty("createdTime") long createdTime,
|
||||
@JsonProperty("accountNo") int accountNo, @JsonProperty("owner") String owner) {
|
||||
super(sequenceId, createdTime, "AccountCreateEvent");
|
||||
this.accountNo = accountNo;
|
||||
this.owner = owner;
|
||||
|
||||
+6
-1
@@ -24,6 +24,8 @@
|
||||
*/
|
||||
package com.iluwatar.event.sourcing.event;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.iluwatar.event.sourcing.state.AccountAggregate;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
@@ -50,7 +52,10 @@ public class MoneyDepositEvent extends DomainEvent {
|
||||
* @param accountNo the account no
|
||||
* @param money the money
|
||||
*/
|
||||
public MoneyDepositEvent(long sequenceId, long createdTime, int accountNo, BigDecimal money) {
|
||||
@JsonCreator
|
||||
public MoneyDepositEvent(@JsonProperty("sequenceId") long sequenceId,
|
||||
@JsonProperty("createdTime") long createdTime,
|
||||
@JsonProperty("accountNo") int accountNo, @JsonProperty("money") BigDecimal money) {
|
||||
super(sequenceId, createdTime, "MoneyDepositEvent");
|
||||
this.money = money;
|
||||
this.accountNo = accountNo;
|
||||
|
||||
+7
-2
@@ -24,6 +24,8 @@
|
||||
*/
|
||||
package com.iluwatar.event.sourcing.event;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.iluwatar.event.sourcing.state.AccountAggregate;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
@@ -52,8 +54,11 @@ public class MoneyTransferEvent extends DomainEvent {
|
||||
* @param accountNoFrom the account no from
|
||||
* @param accountNoTo the account no to
|
||||
*/
|
||||
public MoneyTransferEvent(long sequenceId, long createdTime, BigDecimal money, int accountNoFrom,
|
||||
int accountNoTo) {
|
||||
@JsonCreator
|
||||
public MoneyTransferEvent(@JsonProperty("sequenceId") long sequenceId,
|
||||
@JsonProperty("createdTime") long createdTime,
|
||||
@JsonProperty("money") BigDecimal money, @JsonProperty("accountNoFrom") int accountNoFrom,
|
||||
@JsonProperty("accountNoTo") int accountNoTo) {
|
||||
super(sequenceId, createdTime, "MoneyTransferEvent");
|
||||
this.money = money;
|
||||
this.accountNoFrom = accountNoFrom;
|
||||
|
||||
+9
-5
@@ -28,13 +28,17 @@ import com.iluwatar.event.sourcing.event.DomainEvent;
|
||||
|
||||
/**
|
||||
* This is the implementation of event processor. All events are processed by this class. This
|
||||
* processor uses processorJournal to persist and recover events.
|
||||
* processor uses eventJournal to persist and recover events.
|
||||
*
|
||||
* <p>Created by Serdar Hamzaogullari on 06.08.2017.
|
||||
*/
|
||||
public class DomainEventProcessor {
|
||||
|
||||
private final JsonFileJournal processorJournal = new JsonFileJournal();
|
||||
private final EventJournal eventJournal;
|
||||
|
||||
public DomainEventProcessor(EventJournal eventJournal) {
|
||||
this.eventJournal = eventJournal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process.
|
||||
@@ -43,14 +47,14 @@ public class DomainEventProcessor {
|
||||
*/
|
||||
public void process(DomainEvent domainEvent) {
|
||||
domainEvent.process();
|
||||
processorJournal.write(domainEvent);
|
||||
eventJournal.write(domainEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset.
|
||||
*/
|
||||
public void reset() {
|
||||
processorJournal.reset();
|
||||
eventJournal.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,7 +62,7 @@ public class DomainEventProcessor {
|
||||
*/
|
||||
public void recover() {
|
||||
DomainEvent domainEvent;
|
||||
while ((domainEvent = processorJournal.readNext()) != null) {
|
||||
while ((domainEvent = eventJournal.readNext()) != null) {
|
||||
domainEvent.process();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.iluwatar.event.sourcing.processor;
|
||||
|
||||
import com.iluwatar.event.sourcing.event.DomainEvent;
|
||||
import java.io.File;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Base class for Journaling implementations.
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class EventJournal {
|
||||
|
||||
File file;
|
||||
|
||||
/**
|
||||
* Write.
|
||||
*
|
||||
* @param domainEvent the domain event.
|
||||
*/
|
||||
abstract void write(DomainEvent domainEvent);
|
||||
|
||||
/**
|
||||
* Reset.
|
||||
*/
|
||||
void reset() {
|
||||
if (file.delete()) {
|
||||
LOGGER.info("File cleared successfully............");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read domain event.
|
||||
*
|
||||
* @return the domain event.
|
||||
*/
|
||||
abstract DomainEvent readNext();
|
||||
}
|
||||
+18
-38
@@ -24,9 +24,8 @@
|
||||
*/
|
||||
package com.iluwatar.event.sourcing.processor;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.iluwatar.event.sourcing.event.AccountCreateEvent;
|
||||
import com.iluwatar.event.sourcing.event.DomainEvent;
|
||||
import com.iluwatar.event.sourcing.event.MoneyDepositEvent;
|
||||
@@ -49,9 +48,8 @@ import java.util.List;
|
||||
*
|
||||
* <p>Created by Serdar Hamzaogullari on 06.08.2017.
|
||||
*/
|
||||
public class JsonFileJournal {
|
||||
public class JsonFileJournal extends EventJournal {
|
||||
|
||||
private final File file;
|
||||
private final List<String> events = new ArrayList<>();
|
||||
private int index = 0;
|
||||
|
||||
@@ -81,22 +79,12 @@ public class JsonFileJournal {
|
||||
*
|
||||
* @param domainEvent the domain event
|
||||
*/
|
||||
@Override
|
||||
public void write(DomainEvent domainEvent) {
|
||||
var gson = new Gson();
|
||||
JsonElement jsonElement;
|
||||
if (domainEvent instanceof AccountCreateEvent) {
|
||||
jsonElement = gson.toJsonTree(domainEvent, AccountCreateEvent.class);
|
||||
} else if (domainEvent instanceof MoneyDepositEvent) {
|
||||
jsonElement = gson.toJsonTree(domainEvent, MoneyDepositEvent.class);
|
||||
} else if (domainEvent instanceof MoneyTransferEvent) {
|
||||
jsonElement = gson.toJsonTree(domainEvent, MoneyTransferEvent.class);
|
||||
} else {
|
||||
throw new RuntimeException("Journal Event not recognized");
|
||||
}
|
||||
|
||||
var mapper = new ObjectMapper();
|
||||
try (var output = new BufferedWriter(
|
||||
new OutputStreamWriter(new FileOutputStream(file, true), StandardCharsets.UTF_8))) {
|
||||
var eventString = jsonElement.toString();
|
||||
var eventString = mapper.writeValueAsString(domainEvent);
|
||||
output.write(eventString + "\r\n");
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
@@ -104,14 +92,6 @@ public class JsonFileJournal {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset.
|
||||
*/
|
||||
public void reset() {
|
||||
file.delete();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read the next domain event.
|
||||
*
|
||||
@@ -124,19 +104,19 @@ public class JsonFileJournal {
|
||||
var event = events.get(index);
|
||||
index++;
|
||||
|
||||
var parser = new JsonParser();
|
||||
var jsonElement = parser.parse(event);
|
||||
var eventClassName = jsonElement.getAsJsonObject().get("eventClassName").getAsString();
|
||||
var gson = new Gson();
|
||||
var mapper = new ObjectMapper();
|
||||
DomainEvent domainEvent;
|
||||
if (eventClassName.equals("AccountCreateEvent")) {
|
||||
domainEvent = gson.fromJson(jsonElement, AccountCreateEvent.class);
|
||||
} else if (eventClassName.equals("MoneyDepositEvent")) {
|
||||
domainEvent = gson.fromJson(jsonElement, MoneyDepositEvent.class);
|
||||
} else if (eventClassName.equals("MoneyTransferEvent")) {
|
||||
domainEvent = gson.fromJson(jsonElement, MoneyTransferEvent.class);
|
||||
} else {
|
||||
throw new RuntimeException("Journal Event not recegnized");
|
||||
try {
|
||||
var jsonElement = mapper.readTree(event);
|
||||
var eventClassName = jsonElement.get("eventClassName").asText();
|
||||
domainEvent = switch (eventClassName) {
|
||||
case "AccountCreateEvent" -> mapper.treeToValue(jsonElement, AccountCreateEvent.class);
|
||||
case "MoneyDepositEvent" -> mapper.treeToValue(jsonElement, MoneyDepositEvent.class);
|
||||
case "MoneyTransferEvent" -> mapper.treeToValue(jsonElement, MoneyTransferEvent.class);
|
||||
default -> throw new RuntimeException("Journal Event not recognized");
|
||||
};
|
||||
} catch (JsonProcessingException jsonProcessingException) {
|
||||
throw new RuntimeException("Failed to convert JSON");
|
||||
}
|
||||
|
||||
domainEvent.setRealTime(false);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import static com.iluwatar.event.sourcing.app.App.ACCOUNT_OF_DAENERYS;
|
||||
import static com.iluwatar.event.sourcing.app.App.ACCOUNT_OF_JON;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -30,6 +31,7 @@ import com.iluwatar.event.sourcing.event.AccountCreateEvent;
|
||||
import com.iluwatar.event.sourcing.event.MoneyDepositEvent;
|
||||
import com.iluwatar.event.sourcing.event.MoneyTransferEvent;
|
||||
import com.iluwatar.event.sourcing.processor.DomainEventProcessor;
|
||||
import com.iluwatar.event.sourcing.processor.JsonFileJournal;
|
||||
import com.iluwatar.event.sourcing.state.AccountAggregate;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
@@ -53,7 +55,7 @@ class IntegrationTest {
|
||||
*/
|
||||
@BeforeEach
|
||||
void initialize() {
|
||||
eventProcessor = new DomainEventProcessor();
|
||||
eventProcessor = new DomainEventProcessor(new JsonFileJournal());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +86,7 @@ class IntegrationTest {
|
||||
|
||||
AccountAggregate.resetState();
|
||||
|
||||
eventProcessor = new DomainEventProcessor();
|
||||
eventProcessor = new DomainEventProcessor(new JsonFileJournal());
|
||||
eventProcessor.recover();
|
||||
|
||||
var accountOfDaenerysAfterShotDown = AccountAggregate.getAccount(ACCOUNT_OF_DAENERYS);
|
||||
|
||||
Reference in New Issue
Block a user