diff --git a/README.md b/README.md index 2f3956a..f5735b6 100644 --- a/README.md +++ b/README.md @@ -157,4 +157,4 @@ void givenDwellingWith2Creatures_WhenRecruit2Creatures_ThenRecruited() { If you'd like to hire me for Domain-Driven Design and/or Event Sourcing projects I'm available to work with: Kotlin, Java, C# .NET, Ruby and JavaScript/TypeScript (Node.js or React). -Please reach me out on LinkedIn [linkedin.com/in/mateusznakodach/](https://www.linkedin.com/in/mateusznakodach/). +Please reach me out on LinkedIn [linkedin.com/in/mateusznakodach/](https://www.linkedin.com/in/mateusznakodach/). \ No newline at end of file diff --git a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java index 99753bf..9c1bcdc 100644 --- a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java +++ b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java @@ -1,11 +1,22 @@ package com.dddheroes.heroesofddd.creaturerecruitment; +import com.dddheroes.heroesofddd.creaturerecruitment.write.DwellingId; import com.dddheroes.heroesofddd.creaturerecruitment.write.recruitcreature.RecruitCreature; import com.dddheroes.heroesofddd.resourcespool.application.CommandCostResolver; import com.dddheroes.heroesofddd.shared.domain.valueobjects.Resources; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.axonframework.eventsourcing.EventCountSnapshotTriggerDefinition; +import org.axonframework.eventsourcing.SnapshotTriggerDefinition; +import org.axonframework.eventsourcing.Snapshotter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.io.IOException; + @Configuration class CreatureRecruitmentConfiguration { @@ -23,4 +34,33 @@ public Class supportedCommandType() { } }; } + + @Bean + SnapshotTriggerDefinition dwellingSnapshotTrigger(Snapshotter snapshotter) { + return new EventCountSnapshotTriggerDefinition(snapshotter, 5); + } + + @Bean + public Module dwellingIdSerializationModule() { + return new DwellingIdSerializationModule(); + } + + private static class DwellingIdSerializationModule extends SimpleModule { + + public DwellingIdSerializationModule() { + addSerializer(DwellingId.class, new JsonSerializer<>() { + @Override + public void serialize(DwellingId value, JsonGenerator gen, SerializerProvider __) throws IOException { + gen.writeString(value.raw()); + } + }); + addDeserializer(DwellingId.class, new JsonDeserializer<>() { + @Override + public DwellingId deserialize(JsonParser p, DeserializationContext __) throws IOException { + return new DwellingId(p.getValueAsString()); + } + }); + } + + } } diff --git a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java index 5cf67ad..fd0c495 100644 --- a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java +++ b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java @@ -19,17 +19,21 @@ import org.axonframework.modelling.command.AggregateIdentifier; import org.axonframework.modelling.command.CreationPolicy; import org.axonframework.spring.stereotype.Aggregate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import static org.axonframework.modelling.command.AggregateLifecycle.*; -@Aggregate +@Aggregate(snapshotTriggerDefinition = "dwellingSnapshotTrigger") public class Dwelling { + private static final Logger logger = LoggerFactory.getLogger(Dwelling.class); + @AggregateIdentifier - private DwellingId dwellingId; - private CreatureId creatureId; - private Resources costPerTroop; - private Amount availableCreatures; + public DwellingId dwellingId; // needs to be public for snapshotting + public CreatureId creatureId; + public Resources costPerTroop; + public Amount availableCreatures; @CommandHandler @CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING) // performance downside in comparison to constructor @@ -47,6 +51,7 @@ void decide(BuildDwelling command) { @EventSourcingHandler void evolve(DwellingBuilt event) { + logger.info("🏗️ Dwelling built with ID: {}, creature type: {}", event.dwellingId(), event.creatureId()); this.dwellingId = new DwellingId(event.dwellingId()); this.creatureId = new CreatureId(event.creatureId()); this.costPerTroop = Resources.fromRaw(event.costPerTroop()); @@ -69,15 +74,13 @@ void decide(IncreaseAvailableCreatures command) { @EventSourcingHandler void evolve(AvailableCreaturesChanged event) { + logger.info("📈 Available creatures changed for dwelling {}: {} creatures now available", + event.dwellingId(), event.changedTo()); this.availableCreatures = new Amount(event.changedTo()); } @CommandHandler -// @CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING) void decide(RecruitCreature command) { -// if(dwellingId == null){ -// throw new DomainRule.ViolatedException("Only not built building can be build"); -// } new RecruitCreaturesNotExceedAvailableCreatures( creatureId, availableCreatures, @@ -104,11 +107,16 @@ void decide(RecruitCreature command) { @EventSourcingHandler void evolve(CreatureRecruited event) { + logger.info("🧙 Recruited {} creatures of type {} from dwelling {} to army {}", + event.quantity(), event.creatureId(), event.dwellingId(), event.toArmy()); // todo: consider if it's OK or RecruitCreature should cause also AvailableCreaturesChanged event this.availableCreatures = this.availableCreatures.minus(new Amount(event.quantity())); } Dwelling() { + logger.info("\uD83D\uDC80 Dwelling non-args constructor"); // required by Axon } + + } diff --git a/src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/SerializationConfiguration.java b/src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/SerializationConfiguration.java new file mode 100644 index 0000000..9ba57be --- /dev/null +++ b/src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/SerializationConfiguration.java @@ -0,0 +1,139 @@ +package com.dddheroes.heroesofddd.shared.infrastructure; + +import com.dddheroes.heroesofddd.shared.domain.identifiers.ArmyId; +import com.dddheroes.heroesofddd.shared.domain.identifiers.CreatureId; +import com.dddheroes.heroesofddd.shared.domain.identifiers.GameId; +import com.dddheroes.heroesofddd.shared.domain.identifiers.PlayerId; +import com.dddheroes.heroesofddd.shared.domain.valueobjects.Amount; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; + +@Configuration +public class SerializationConfiguration { + + @Bean + public Module gameIdSerializationModule() { + return new GameIdSerializationModule(); + } + + @Bean + public Module playerIdSerializationModule() { + return new PlayerIdSerializationModule(); + } + + @Bean + public Module creatureIdSerializationModule() { + return new CreatureIdSerializationModule(); + } + + @Bean + public Module armyIdSerializationModule() { + return new ArmyIdSerializationModule(); + } + + @Bean + public Module amountSerializationModule() { + return new AmountSerializationModule(); + } + + private static class GameIdSerializationModule extends SimpleModule { + + public GameIdSerializationModule() { + addSerializer(GameId.class, new JsonSerializer<>() { + @Override + public void serialize(GameId value, JsonGenerator gen, SerializerProvider __) throws IOException { + gen.writeString(value.raw()); + } + }); + addDeserializer(GameId.class, new JsonDeserializer<>() { + @Override + public GameId deserialize(JsonParser p, DeserializationContext __) throws IOException { + return new GameId(p.getValueAsString()); + } + }); + } + + } + + private static class PlayerIdSerializationModule extends SimpleModule { + + public PlayerIdSerializationModule() { + addSerializer(PlayerId.class, new JsonSerializer<>() { + @Override + public void serialize(PlayerId value, JsonGenerator gen, SerializerProvider __) throws IOException { + gen.writeString(value.raw()); + } + }); + addDeserializer(PlayerId.class, new JsonDeserializer<>() { + @Override + public PlayerId deserialize(JsonParser p, DeserializationContext __) throws IOException { + return new PlayerId(p.getValueAsString()); + } + }); + } + } + + private static class CreatureIdSerializationModule extends SimpleModule { + + public CreatureIdSerializationModule() { + addSerializer(CreatureId.class, new JsonSerializer<>() { + @Override + public void serialize(CreatureId value, JsonGenerator gen, SerializerProvider __) throws IOException { + gen.writeString(value.raw()); + } + }); + addDeserializer(CreatureId.class, new JsonDeserializer<>() { + @Override + public CreatureId deserialize(JsonParser p, DeserializationContext __) throws IOException { + return new CreatureId(p.getValueAsString()); + } + }); + } + + } + + private static class ArmyIdSerializationModule extends SimpleModule { + + public ArmyIdSerializationModule() { + addSerializer(ArmyId.class, new JsonSerializer<>() { + @Override + public void serialize(ArmyId value, JsonGenerator gen, SerializerProvider __) throws IOException { + gen.writeString(value.raw()); + } + }); + addDeserializer(ArmyId.class, new JsonDeserializer<>() { + @Override + public ArmyId deserialize(JsonParser p, DeserializationContext __) throws IOException { + return new ArmyId(p.getValueAsString()); + } + }); + } + + } + + private static class AmountSerializationModule extends SimpleModule { + + public AmountSerializationModule() { + addSerializer(Amount.class, new JsonSerializer<>() { + @Override + public void serialize(Amount value, JsonGenerator gen, SerializerProvider __) throws IOException { + gen.writeNumber(value.raw()); + } + }); + addDeserializer(Amount.class, new JsonDeserializer<>() { + @Override + public Amount deserialize(JsonParser p, DeserializationContext __) throws IOException { + return new Amount(p.getValueAsInt()); + } + }); + } + + } +} diff --git a/src/main/resources/application-jpa-eventstore.yaml b/src/main/resources/application-jpa-eventstore.yaml new file mode 100644 index 0000000..6eb6dab --- /dev/null +++ b/src/main/resources/application-jpa-eventstore.yaml @@ -0,0 +1,11 @@ +axon: + axonserver: + enabled: false +# eventhandling: +# processors: + # (Optional) You can override processor settings here if needed +# eventstore: + # No need to specify storage-engine if axon-server is disabled and JPA is on classpath + # Axon will auto-configure JpaEventStorageEngine + +# Inherit datasource and JPA config from application.yaml \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index b767eae..71f7b8e 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -17,10 +17,12 @@ spring: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect axon: - serializer: - general: jackson axonserver: enabled: true + serializer: + general: jackson + events: jackson + messages: jackson eventhandling: processors: Automation_WhenCreatureRecruitedThenAddToArmy_Processor: