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:
Saiful Haque authored and GitHub committed 2023-07-04 18:22:13 +03:00
1 parent 3636e9c8af
commit 4808865c0b
9 files changed
+101 -62

No files matched your search

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