From 614c3d0466a36483c600e0bf4fda78c41e359700 Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 9 Jun 2026 11:15:56 +0800 Subject: [PATCH 01/10] Add new dependencies for pipeline, domain-driven design, and spring; update junit and maven-surefire plugin versions to use property variables --- sample/pom.xml | 20 ++++++++++++++++++++ uow/pom.xml | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/sample/pom.xml b/sample/pom.xml index 7d9701b..7ca1c3d 100644 --- a/sample/pom.xml +++ b/sample/pom.xml @@ -35,6 +35,21 @@ osba 1.0.0 + + com.euonia + pipeline + 1.0.0 + + + com.euonia + domain-driven-design + 1.0.0 + + + com.euonia + spring + 1.0.0 + org.springframework.boot spring-boot-starter-data-jpa @@ -54,6 +69,11 @@ mysql-connector-j runtime + + com.h2database + h2 + runtime + org.projectlombok lombok diff --git a/uow/pom.xml b/uow/pom.xml index 178f1f1..6fa3064 100644 --- a/uow/pom.xml +++ b/uow/pom.xml @@ -23,7 +23,7 @@ org.junit.jupiter junit-jupiter - 5.12.2 + ${junit.jupiter.version} test @@ -33,7 +33,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.5.3 + ${maven.surefire.plugin.version} From de7910d32f790710c4fcff4c693afd26164765d6 Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 9 Jun 2026 14:56:45 +0800 Subject: [PATCH 02/10] Add application service architecture with use case interfaces and presenter implementation --- .../application/ApplicationService.java | 4 ++ .../application/BaseApplicationService.java | 22 ++++++ .../main/java/com/euonia/usecase/UseCase.java | 18 +++++ .../com/euonia/usecase/UseCaseFailure.java | 14 ++++ .../com/euonia/usecase/UseCasePresenter.java | 72 +++++++++++++++++++ .../com/euonia/usecase/UseCaseSuccess.java | 16 +++++ 6 files changed, 146 insertions(+) create mode 100644 ddd/src/main/java/com/euonia/application/ApplicationService.java create mode 100644 ddd/src/main/java/com/euonia/application/BaseApplicationService.java create mode 100644 ddd/src/main/java/com/euonia/usecase/UseCase.java create mode 100644 ddd/src/main/java/com/euonia/usecase/UseCaseFailure.java create mode 100644 ddd/src/main/java/com/euonia/usecase/UseCasePresenter.java create mode 100644 ddd/src/main/java/com/euonia/usecase/UseCaseSuccess.java diff --git a/ddd/src/main/java/com/euonia/application/ApplicationService.java b/ddd/src/main/java/com/euonia/application/ApplicationService.java new file mode 100644 index 0000000..a380ac1 --- /dev/null +++ b/ddd/src/main/java/com/euonia/application/ApplicationService.java @@ -0,0 +1,4 @@ +package com.euonia.application; + +public interface ApplicationService { +} diff --git a/ddd/src/main/java/com/euonia/application/BaseApplicationService.java b/ddd/src/main/java/com/euonia/application/BaseApplicationService.java new file mode 100644 index 0000000..fd46c5c --- /dev/null +++ b/ddd/src/main/java/com/euonia/application/BaseApplicationService.java @@ -0,0 +1,22 @@ +package com.euonia.application; + +import com.euonia.reflection.ServiceResolver; +import com.euonia.security.UserPrincipal; + +/** + * BaseApplicationService is an abstract class that provides common functionality for application services. + * It implements the ApplicationService interface and uses a ServiceResolver to access other services, such as UserPrincipal, which represents the currently authenticated user. + * This class can be extended by concrete application service implementations to inherit the ability to resolve services and access user information without needing to implement these features in each service. + */ +public abstract class BaseApplicationService implements ApplicationService { + + protected final ServiceResolver serviceResolver; + + protected BaseApplicationService(ServiceResolver serviceResolver) { + this.serviceResolver = serviceResolver; + } + + public UserPrincipal getUser() { + return serviceResolver.getService(UserPrincipal.class); + } +} diff --git a/ddd/src/main/java/com/euonia/usecase/UseCase.java b/ddd/src/main/java/com/euonia/usecase/UseCase.java new file mode 100644 index 0000000..7c9f33a --- /dev/null +++ b/ddd/src/main/java/com/euonia/usecase/UseCase.java @@ -0,0 +1,18 @@ +package com.euonia.usecase; + +/** + * Represents a use case in the application. A use case is a specific operation or action that can be performed within the system. + * It defines a contract for executing a particular business logic or functionality. + * + * @param the type of the input to the use case + * @param the type of the output of the use case + */ +public interface UseCase { + /** + * Executes the use case with the given input and returns the output. + * + * @param input the input data for the use case + * @return the output data from the use case + */ + O execute(I input); +} diff --git a/ddd/src/main/java/com/euonia/usecase/UseCaseFailure.java b/ddd/src/main/java/com/euonia/usecase/UseCaseFailure.java new file mode 100644 index 0000000..7c8038d --- /dev/null +++ b/ddd/src/main/java/com/euonia/usecase/UseCaseFailure.java @@ -0,0 +1,14 @@ +package com.euonia.usecase; + +/** + * Represents a failure output of a use case execution. + * This interface defines a method to handle errors that occur during the execution of a use case. + */ +public interface UseCaseFailure { + /** + * Indicates that the use case execution has failed with an error. + * + * @param throwable the error that caused the failure + */ + void error(Throwable throwable); +} diff --git a/ddd/src/main/java/com/euonia/usecase/UseCasePresenter.java b/ddd/src/main/java/com/euonia/usecase/UseCasePresenter.java new file mode 100644 index 0000000..a00cb4a --- /dev/null +++ b/ddd/src/main/java/com/euonia/usecase/UseCasePresenter.java @@ -0,0 +1,72 @@ +package com.euonia.usecase; + +import java.util.concurrent.Flow.*; +import java.util.concurrent.SubmissionPublisher; +import java.util.function.Consumer; + +/** + * A presenter that implements the output ports for a use case and allows subscribers to listen for success and failure events. + * + * @param the type of the successful output of the use case + */ +public class UseCasePresenter implements UseCaseSuccess, UseCaseFailure, AutoCloseable { + private final Publisher publisher = new SubmissionPublisher<>(); + + private O output; + + /** + * Subscribes to the presenter to receive success and failure events. + * + * @param onSuccess the consumer to handle successful output + * @param onFailure the consumer to handle errors + */ + public void subscribe(Consumer onSuccess, Consumer onFailure) { + publisher.subscribe(new Subscriber<>() { + + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(O item) { + onSuccess.accept(item); + } + + @Override + public void onError(Throwable throwable) { + onFailure.accept(throwable); + } + + @Override + public void onComplete() { + // Handle completion if needed + } + }); + } + + @Override + public void success(O output) { + this.output = output; + ((SubmissionPublisher) publisher).submit(output); + } + + @Override + public void error(Throwable throwable) { + ((SubmissionPublisher) publisher).closeExceptionally(throwable); + } + + @Override + public void close() throws Exception { + ((SubmissionPublisher) publisher).close(); + } + + /** + * Returns the output of the use case execution if it was successful. + * + * @return the output of the use case execution + */ + public O getOutput() { + return output; + } +} diff --git a/ddd/src/main/java/com/euonia/usecase/UseCaseSuccess.java b/ddd/src/main/java/com/euonia/usecase/UseCaseSuccess.java new file mode 100644 index 0000000..45527b7 --- /dev/null +++ b/ddd/src/main/java/com/euonia/usecase/UseCaseSuccess.java @@ -0,0 +1,16 @@ +package com.euonia.usecase; + +/** + * Represents a successful output of a use case execution. + * This interface defines a method to handle the result of a successful use case execution. + * + * @param the type of the successful output + */ +public interface UseCaseSuccess { + /** + * Indicates that the use case execution has succeeded with a result. + * + * @param output the result of the successful execution + */ + void success(O output); +} From 93863650e09a0599981dfca1f714aa4e28597371 Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 9 Jun 2026 16:37:51 +0800 Subject: [PATCH 03/10] Refactor BusinessObjectFactory to use ServiceResolver for service resolution --- .../com/euonia/factory/BusinessObjectFactory.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osba/src/main/java/com/euonia/factory/BusinessObjectFactory.java b/osba/src/main/java/com/euonia/factory/BusinessObjectFactory.java index c632c36..ec620e6 100644 --- a/osba/src/main/java/com/euonia/factory/BusinessObjectFactory.java +++ b/osba/src/main/java/com/euonia/factory/BusinessObjectFactory.java @@ -5,12 +5,12 @@ import com.euonia.osba.*; import com.euonia.osba.abstracts.UseBusinessContext; import com.euonia.reflection.ObjectReflector; +import com.euonia.reflection.ServiceResolver; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Objects; -import java.util.function.Function; /** * BusinessObjectFactory is an implementation of the ObjectFactory interface that uses reflection to create, fetch, insert, update, save, execute, and delete business objects based on annotated factory methods. @@ -19,16 +19,16 @@ @SuppressWarnings("UnusedReturnValue") public class BusinessObjectFactory implements ObjectFactory { - private Function, ?> beanFactory; + private ServiceResolver resolver; /** * Configures the BusinessObjectFactory to use the provided bean factory for object instantiation. * - * @param beanFactory the bean factory function to use for creating objects + * @param resolver the ServiceResolver to be used for resolving services, including the bean factory * @return the current instance of BusinessObjectFactory */ - public BusinessObjectFactory use(Function, ?> beanFactory) { - this.beanFactory = beanFactory; + public BusinessObjectFactory use(ServiceResolver resolver) { + this.resolver = resolver; return this; } @@ -173,10 +173,10 @@ public void delete(Class type, Object... criteria) { @SuppressWarnings("unchecked") private T getObjectInstance(Class type) { T object = PriorityValueFinder.find(queue -> { - if (beanFactory != null) { + if (resolver != null) { queue.add(() -> { try { - return (T) beanFactory.apply(type); + return resolver.getService(type); } catch (Exception e) { return null; } From 70cb10cd7572a04e2e5bdb09527510ff3be82420 Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 9 Jun 2026 16:38:24 +0800 Subject: [PATCH 04/10] Add domain-driven design interfaces and base classes for aggregates and events --- .../java/com/euonia/domain/Aggregate.java | 8 +++ .../java/com/euonia/domain/AggregateBase.java | 35 +++++++++++- .../main/java/com/euonia/domain/Entity.java | 20 ++++++- .../com/euonia/domain/HasDomainEvents.java | 57 +++++++++++++++++++ 4 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 ddd/src/main/java/com/euonia/domain/HasDomainEvents.java diff --git a/ddd/src/main/java/com/euonia/domain/Aggregate.java b/ddd/src/main/java/com/euonia/domain/Aggregate.java index d4bb2c6..cc0669d 100644 --- a/ddd/src/main/java/com/euonia/domain/Aggregate.java +++ b/ddd/src/main/java/com/euonia/domain/Aggregate.java @@ -1,4 +1,12 @@ package com.euonia.domain; +/** + * The Aggregate interface represents an aggregate root in a domain-driven design (DDD) context. + * An aggregate is a cluster of domain objects that can be treated as a single unit for data changes. + * The aggregate root is the main entity that controls access to the other entities within the aggregate and is responsible for enforcing the consistency of the aggregate as a whole. + * + * @param the type of the identifier for the aggregate + */ public interface Aggregate> extends Entity { + } diff --git a/ddd/src/main/java/com/euonia/domain/AggregateBase.java b/ddd/src/main/java/com/euonia/domain/AggregateBase.java index d9c7147..be15d4b 100644 --- a/ddd/src/main/java/com/euonia/domain/AggregateBase.java +++ b/ddd/src/main/java/com/euonia/domain/AggregateBase.java @@ -3,23 +3,52 @@ import com.euonia.domain.event.DomainEventBase; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.function.Consumer; -public abstract class AggregateBase> extends EntityBase implements Aggregate { +/** + * The AggregateBase class is an abstract base class for implementing the Aggregate interface in a domain-driven design (DDD) context. + * It provides a common implementation for managing domain events and their handlers within an aggregate root. + * An aggregate is a cluster of domain objects that can be treated as a single unit for data changes. + * The aggregate root is the main entity that controls access to the other entities within the aggregate and is responsible for enforcing the consistency of the aggregate as a whole. + * + * @param the type of the identifier for the aggregate, which must be comparable. + */ +public abstract class AggregateBase> extends EntityBase implements Aggregate, HasDomainEvents { private final List events = new ArrayList<>(); + private final Map, Consumer> eventHandlers = new HashMap<>(); + public void registerEvent(Class eventType, Consumer handler) { + eventHandlers.put(eventType, event -> handler.accept(eventType.cast(event))); + } + + @Override public List getEvents() { return List.copyOf(events); } - protected void raiseEvent(E event) { + @Override + public void raiseEvent(E event) { + applyEvent(event); events.add(event); } - protected void clearEvents() { + @Override + public void applyEvent(E event) { + var handler = eventHandlers.getOrDefault(event.getClass(), null); + if (handler != null) { + handler.accept(event); + } + } + + @Override + public void clearEvents() { events.clear(); } + @Override public void attachEvents() { for (DomainEventBase event : events) { event.attach(this); diff --git a/ddd/src/main/java/com/euonia/domain/Entity.java b/ddd/src/main/java/com/euonia/domain/Entity.java index b11b730..8c90e7f 100644 --- a/ddd/src/main/java/com/euonia/domain/Entity.java +++ b/ddd/src/main/java/com/euonia/domain/Entity.java @@ -6,9 +6,27 @@ * They typically represent real-world concepts or objects in the system and are often used to model business entities or domain objects. */ public interface Entity> { + + /** + * Gets the identifier for the entity. The identifier is a unique value that distinguishes this entity from others. + * + * @return the unique identifier of the entity + */ ID getId(); + /** + * Sets the identifier for the entity. The identifier is a unique value that distinguishes this entity from others. + * + * @param id the unique identifier to set for this entity + */ void setId(ID id); - Object[] getKeys(); + /** + * Gets the keys that uniquely identify this entity. By default, it returns an array containing the entity's identifier. + * + * @return an array of objects representing the keys that uniquely identify this entity + */ + default Object[] getKeys() { + return new Object[]{getId()}; + } } diff --git a/ddd/src/main/java/com/euonia/domain/HasDomainEvents.java b/ddd/src/main/java/com/euonia/domain/HasDomainEvents.java new file mode 100644 index 0000000..b997a6f --- /dev/null +++ b/ddd/src/main/java/com/euonia/domain/HasDomainEvents.java @@ -0,0 +1,57 @@ +package com.euonia.domain; + +import com.euonia.domain.event.DomainEventBase; + +import java.util.List; +import java.util.function.Consumer; + +/** + * The HasDomainEvents interface defines the contract for an aggregate that can raise and manage domain events in a domain-driven design (DDD) context. + * An aggregate is a cluster of domain objects that can be treated as a single unit for data changes. + * The aggregate root is the main entity that controls access to the other entities within the aggregate and is responsible for enforcing the consistency of the aggregate as a whole. + * Domain events represent significant occurrences or changes in the state of the aggregate that are relevant to the business logic and can be used for event sourcing, auditing, or integration with other systems. + */ +public interface HasDomainEvents { + /** + * Gets the list of domain events that have been raised by this aggregate. + * Domain events represent significant occurrences or changes in the state of the aggregate that are relevant to the business logic and can be used for event sourcing, auditing, or integration with other systems. + * + * @return the list of domain events raised by this aggregate + */ + List getEvents(); + + /** + * Registers a handler for a specific type of domain event. The handler will be invoked when an event of the specified type is raised. + * + * @param eventType the type of domain event to register the handler for + * @param handler the handler to be invoked when the event is raised + * @param the type of the domain event + */ + void registerEvent(Class eventType, Consumer handler); + + /** + * Raises a domain event. The event will be processed by the registered handlers. + * + * @param event the domain event to raise + * @param the type of the domain event + */ + void raiseEvent(E event); + + /** + * Applies a domain event to the aggregate. This method is typically used to update the state of the aggregate based on the event. + * + * @param event the domain event to apply + * @param the type of the domain event + */ + void applyEvent(E event); + + /** + * Clears all domain events that have been raised by this aggregate. + */ + void clearEvents(); + + /** + * Attaches all domain events to the aggregate. This method is typically used to initialize the aggregate with existing events. + */ + void attachEvents(); +} From b8030b2c94b5f84dc89d54a9b6924353ade3c418 Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 9 Jun 2026 16:40:17 +0800 Subject: [PATCH 05/10] Add PipelineDemoController and UserController with user management functionality --- .../com/euonia/sample/SampleApplication.java | 3 + .../controller/PipelineDemoController.java | 69 ++++++++++++ .../sample/controller/UserController.java | 33 ++++++ .../euonia/sample/domain/aggregate/User.java | 100 +++++++++++++++++- .../sample/persistent/UserRepository.java | 4 + sample/src/main/resources/application.yaml | 15 +++ 6 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 sample/src/main/java/com/euonia/sample/controller/PipelineDemoController.java create mode 100644 sample/src/main/java/com/euonia/sample/controller/UserController.java create mode 100644 sample/src/main/java/com/euonia/sample/persistent/UserRepository.java diff --git a/sample/src/main/java/com/euonia/sample/SampleApplication.java b/sample/src/main/java/com/euonia/sample/SampleApplication.java index 6f3ee2c..5ed9a0c 100644 --- a/sample/src/main/java/com/euonia/sample/SampleApplication.java +++ b/sample/src/main/java/com/euonia/sample/SampleApplication.java @@ -1,9 +1,12 @@ package com.euonia.sample; +//import com.euonia.pipeline.spring.PipelineConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; @SpringBootApplication +//@Import(PipelineConfiguration.class) public class SampleApplication { public static void main(String[] args) { diff --git a/sample/src/main/java/com/euonia/sample/controller/PipelineDemoController.java b/sample/src/main/java/com/euonia/sample/controller/PipelineDemoController.java new file mode 100644 index 0000000..b1b2203 --- /dev/null +++ b/sample/src/main/java/com/euonia/sample/controller/PipelineDemoController.java @@ -0,0 +1,69 @@ +package com.euonia.sample.controller; + +import com.euonia.pipeline.PipelineFactory; +import com.euonia.pipeline.RequestResponsePipelineBehavior; +import com.euonia.pipeline.RequestResponsePipelineDelegate; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import com.euonia.pipeline.PipelineBehaviors; + +@RestController +@RequestMapping("/api/pipeline") +public class PipelineDemoController { + private final PipelineFactory pipelineFactory; + + public PipelineDemoController(PipelineFactory pipelineFactory) { + this.pipelineFactory = pipelineFactory; + } + + @GetMapping("/echo") + public ResponseEntity> echo(@RequestParam(defaultValue = "hello") String value) { + EchoRequest request = new EchoRequest(value); + + var pipeline = pipelineFactory.createRequestResponse(); + + pipeline.use(EchoSuffixBehavior.class); + + String result = pipeline.runAsync(request, r -> CompletableFuture.completedFuture(r.value())) + .toCompletableFuture() + .join(); + + Map response = new LinkedHashMap<>(); + response.put("input", value); + response.put("output", result); + return ResponseEntity.ok(response); + } + + @PipelineBehaviors({EchoUpperCaseBehavior.class}) + private record EchoRequest(String value) { + } + + private static class EchoUpperCaseBehavior implements RequestResponsePipelineBehavior { + @Override + public CompletionStage handleAsync(EchoRequest context, RequestResponsePipelineDelegate next) { + return next.invoke(new EchoRequest(context.value().toUpperCase())); + } + } + + private static class EchoSuffixBehavior { + private final RequestResponsePipelineDelegate next; + + private EchoSuffixBehavior(RequestResponsePipelineDelegate next) { + this.next = next; + } + + public CompletionStage handleAsync(EchoRequest context) { + return next.invoke(context).thenApply(value -> value + "-pipeline"); + } + } +} diff --git a/sample/src/main/java/com/euonia/sample/controller/UserController.java b/sample/src/main/java/com/euonia/sample/controller/UserController.java new file mode 100644 index 0000000..6c973f7 --- /dev/null +++ b/sample/src/main/java/com/euonia/sample/controller/UserController.java @@ -0,0 +1,33 @@ +package com.euonia.sample.controller; + +import com.euonia.factory.ObjectFactory; +import com.euonia.sample.domain.aggregate.User; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.annotation.RequestScope; + +import java.util.Map; + +@RestController +@RequestMapping("/api/user") +@RequestScope +public class UserController { + private final ObjectFactory factory; + + public UserController(ObjectFactory factory) { + this.factory = factory; + } + + @PostMapping() + public ResponseEntity createUser(@RequestBody Map params) { + var user = factory.create(User.class, params.getOrDefault("name", "Default Name")); + user.save(false); + return ResponseEntity.ok("User created with name: " + user.getName()); + } + + @GetMapping("{id}") + public ResponseEntity getUser(@PathVariable long id) { + var user = factory.fetch(User.class, id); + return ResponseEntity.ok("User fetched with name: " + user.getName()); + } +} diff --git a/sample/src/main/java/com/euonia/sample/domain/aggregate/User.java b/sample/src/main/java/com/euonia/sample/domain/aggregate/User.java index 00e74f2..086bf8b 100644 --- a/sample/src/main/java/com/euonia/sample/domain/aggregate/User.java +++ b/sample/src/main/java/com/euonia/sample/domain/aggregate/User.java @@ -1,16 +1,108 @@ package com.euonia.sample.domain.aggregate; +import com.euonia.annotation.Required; +import com.euonia.core.ObjectId; +import com.euonia.domain.Aggregate; +import com.euonia.factory.annotation.FactoryCreate; import com.euonia.osba.EditableObject; +import com.euonia.osba.rules.LambdaRule; +import com.euonia.osba.rules.RuleBase; +import com.euonia.osba.rules.RuleContext; +import com.euonia.reflection.DisplayName; +import com.euonia.reflection.PropertyInfo; +import com.euonia.sample.persistent.UserRepository; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; -public class User extends EditableObject { +import java.util.Objects; +import java.util.concurrent.CompletableFuture; - private String name; +@Component +@Scope("prototype") +public class User extends EditableObject implements Aggregate { + + private final PropertyInfo id = registerProperty(Long.class, "id"); + @DisplayName("User Name") + @Required(message = "name is valid") + private final PropertyInfo name = registerProperty(String.class, "name"); + + @DisplayName("User age") + private final PropertyInfo age = registerProperty(Integer.class, "age"); + + public Long getId() { + return getProperty(id); + } + + public void setId(Long id) { + loadProperty(this.id, id); + } public String getName() { - return name; + return getProperty(this.name); } public void setName(String name) { - this.name = name; + setProperty(this.name, name); + } + + public int getAge() { + return getProperty(age); + } + + public void setAge(int age) { + setProperty(this.age, age); + } + + @Override + protected void addRules() { + super.addRules(); + getRules().addRule(new UserNameRule(this.name)); + getRules().addRule(new LambdaRule<>(this.age, (age, context) -> age != null && age >= 18, "Age must be at least 18")); + } + + @FactoryCreate + protected void create(String name) { + super.create(); + setName(name); + setId(Objects.requireNonNull(ObjectId.snowflake().getValue(Long.class))); + } + + @Override + protected void insert() { + super.insert(); + var repository = getBusinessContext().getOrCreateObject(UserRepository.class); + } + + protected void fetch(long id) { + try (var x = bypassRuleChecks()) { + setName("Test User"); + } catch (Exception ex) { + // + } + } + + public class UserNameRule extends RuleBase { + + public UserNameRule(PropertyInfo property) { + super(property); + } + + @Override + public CompletableFuture executeAsync(RuleContext context) { + return CompletableFuture.runAsync(() -> { + + if (!(context.getTarget() instanceof User user)) { + return; + } + + var name = user.getName(); + + if (name == null || name.trim().isEmpty()) { + context.addErrorResult(String.format("%s cannot be empty", getProperty().getFriendlyName())); + } else if (name.length() < 12) { + context.addErrorResult(String.format("%s must be 12 characters", getProperty().getFriendlyName())); + } + }); + } } } diff --git a/sample/src/main/java/com/euonia/sample/persistent/UserRepository.java b/sample/src/main/java/com/euonia/sample/persistent/UserRepository.java new file mode 100644 index 0000000..03e7250 --- /dev/null +++ b/sample/src/main/java/com/euonia/sample/persistent/UserRepository.java @@ -0,0 +1,4 @@ +package com.euonia.sample.persistent; + +public interface UserRepository { +} diff --git a/sample/src/main/resources/application.yaml b/sample/src/main/resources/application.yaml index 6061b5a..38d0772 100644 --- a/sample/src/main/resources/application.yaml +++ b/sample/src/main/resources/application.yaml @@ -1,3 +1,18 @@ spring: application: name: sample + datasource: + url: jdbc:h2:mem:linkyoudb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + hibernate: + ddl-auto: update + show-sql: true + web: + error: + include-exception: true + include-message: always + include-binding-errors: always + include-stacktrace: on-param From f9f69ef777d14ff3191e7ef9432f73b57a97394a Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 9 Jun 2026 16:40:48 +0800 Subject: [PATCH 06/10] Add OsbaConfiguration for ObjectFactory bean and update AutoConfiguration imports --- spring/pom.xml | 6 ++++++ .../java/com/euonia/osba/OsbaConfiguration.java | 17 +++++++++++++++++ ...boot.autoconfigure.AutoConfiguration.imports | 1 + 3 files changed, 24 insertions(+) create mode 100644 spring/src/main/java/com/euonia/osba/OsbaConfiguration.java diff --git a/spring/pom.xml b/spring/pom.xml index 0489775..8094708 100644 --- a/spring/pom.xml +++ b/spring/pom.xml @@ -19,6 +19,12 @@ ${revision} + + com.euonia + osba + ${revision} + + com.euonia unit-of-work diff --git a/spring/src/main/java/com/euonia/osba/OsbaConfiguration.java b/spring/src/main/java/com/euonia/osba/OsbaConfiguration.java new file mode 100644 index 0000000..22658c8 --- /dev/null +++ b/spring/src/main/java/com/euonia/osba/OsbaConfiguration.java @@ -0,0 +1,17 @@ +package com.euonia.osba; + +import com.euonia.factory.BusinessObjectFactory; +import com.euonia.factory.ObjectFactory; +import com.euonia.reflection.ServiceResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OsbaConfiguration { + @Bean + public ObjectFactory objectFactory(ServiceResolver resolver) { + var factory = new BusinessObjectFactory(); + factory.use(resolver); + return factory; + } +} diff --git a/spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index f144323..b48ac21 100644 --- a/spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,2 +1,3 @@ com.euonia.uow.UnitOfWorkAutoConfiguration com.euonia.reflection.ServiceResolverConfiguration +com.euonia.osba.OsbaConfiguration From 705f76484100cb233d768c076d26fc934e7a96ef Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 9 Jun 2026 16:53:06 +0800 Subject: [PATCH 07/10] Refactor AggregateBase to use ConcurrentMap for event handlers and support multiple handlers per event type --- .../java/com/euonia/domain/AggregateBase.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ddd/src/main/java/com/euonia/domain/AggregateBase.java b/ddd/src/main/java/com/euonia/domain/AggregateBase.java index be15d4b..aeb37b9 100644 --- a/ddd/src/main/java/com/euonia/domain/AggregateBase.java +++ b/ddd/src/main/java/com/euonia/domain/AggregateBase.java @@ -3,9 +3,9 @@ import com.euonia.domain.event.DomainEventBase; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.function.Consumer; /** @@ -18,10 +18,11 @@ */ public abstract class AggregateBase> extends EntityBase implements Aggregate, HasDomainEvents { private final List events = new ArrayList<>(); - private final Map, Consumer> eventHandlers = new HashMap<>(); + private final ConcurrentMap, List>> eventHandlers = new ConcurrentHashMap<>(); public void registerEvent(Class eventType, Consumer handler) { - eventHandlers.put(eventType, event -> handler.accept(eventType.cast(event))); + eventHandlers.computeIfAbsent(eventType, k -> new ArrayList<>()) + .add(event -> handler.accept(eventType.cast(event))); } @Override @@ -37,9 +38,9 @@ public void raiseEvent(E event) { @Override public void applyEvent(E event) { - var handler = eventHandlers.getOrDefault(event.getClass(), null); - if (handler != null) { - handler.accept(event); + var handlers = eventHandlers.getOrDefault(event.getClass(), null); + if (handlers != null) { + handlers.forEach(handler -> handler.accept(event)); } } From f4c80956339078fa7b736941c32c08f8df46f3a3 Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 9 Jun 2026 17:05:56 +0800 Subject: [PATCH 08/10] Refactor AggregateBase and HasDomainEvents to use DomainEvent instead of DomainEventBase --- .../java/com/euonia/domain/AggregateBase.java | 16 ++++++++-------- .../java/com/euonia/domain/HasDomainEvents.java | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ddd/src/main/java/com/euonia/domain/AggregateBase.java b/ddd/src/main/java/com/euonia/domain/AggregateBase.java index aeb37b9..b27e37f 100644 --- a/ddd/src/main/java/com/euonia/domain/AggregateBase.java +++ b/ddd/src/main/java/com/euonia/domain/AggregateBase.java @@ -1,6 +1,6 @@ package com.euonia.domain; -import com.euonia.domain.event.DomainEventBase; +import com.euonia.domain.event.DomainEvent; import java.util.ArrayList; import java.util.List; @@ -17,27 +17,27 @@ * @param the type of the identifier for the aggregate, which must be comparable. */ public abstract class AggregateBase> extends EntityBase implements Aggregate, HasDomainEvents { - private final List events = new ArrayList<>(); - private final ConcurrentMap, List>> eventHandlers = new ConcurrentHashMap<>(); + private final List events = new ArrayList<>(); + private final ConcurrentMap, List>> eventHandlers = new ConcurrentHashMap<>(); - public void registerEvent(Class eventType, Consumer handler) { + public void registerEvent(Class eventType, Consumer handler) { eventHandlers.computeIfAbsent(eventType, k -> new ArrayList<>()) .add(event -> handler.accept(eventType.cast(event))); } @Override - public List getEvents() { + public List getEvents() { return List.copyOf(events); } @Override - public void raiseEvent(E event) { + public void raiseEvent(E event) { applyEvent(event); events.add(event); } @Override - public void applyEvent(E event) { + public void applyEvent(E event) { var handlers = eventHandlers.getOrDefault(event.getClass(), null); if (handlers != null) { handlers.forEach(handler -> handler.accept(event)); @@ -51,7 +51,7 @@ public void clearEvents() { @Override public void attachEvents() { - for (DomainEventBase event : events) { + for (DomainEvent event : events) { event.attach(this); } } diff --git a/ddd/src/main/java/com/euonia/domain/HasDomainEvents.java b/ddd/src/main/java/com/euonia/domain/HasDomainEvents.java index b997a6f..0649879 100644 --- a/ddd/src/main/java/com/euonia/domain/HasDomainEvents.java +++ b/ddd/src/main/java/com/euonia/domain/HasDomainEvents.java @@ -1,6 +1,6 @@ package com.euonia.domain; -import com.euonia.domain.event.DomainEventBase; +import com.euonia.domain.event.DomainEvent; import java.util.List; import java.util.function.Consumer; @@ -18,7 +18,7 @@ public interface HasDomainEvents { * * @return the list of domain events raised by this aggregate */ - List getEvents(); + List getEvents(); /** * Registers a handler for a specific type of domain event. The handler will be invoked when an event of the specified type is raised. @@ -27,7 +27,7 @@ public interface HasDomainEvents { * @param handler the handler to be invoked when the event is raised * @param the type of the domain event */ - void registerEvent(Class eventType, Consumer handler); + void registerEvent(Class eventType, Consumer handler); /** * Raises a domain event. The event will be processed by the registered handlers. @@ -35,7 +35,7 @@ public interface HasDomainEvents { * @param event the domain event to raise * @param the type of the domain event */ - void raiseEvent(E event); + void raiseEvent(E event); /** * Applies a domain event to the aggregate. This method is typically used to update the state of the aggregate based on the event. @@ -43,7 +43,7 @@ public interface HasDomainEvents { * @param event the domain event to apply * @param the type of the domain event */ - void applyEvent(E event); + void applyEvent(E event); /** * Clears all domain events that have been raised by this aggregate. From 6e0b8673971b9a8f857b64516ff22692c05ea77d Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 9 Jun 2026 17:06:37 +0800 Subject: [PATCH 09/10] Implement User entity with event raising and introduce EditableObjectBase for domain events --- .../sample/domain/EditableObjectBase.java | 57 +++++++++++++++++++ .../euonia/sample/domain/aggregate/User.java | 13 ++--- .../sample/domain/event/UserCreatedEvent.java | 13 +++++ 3 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 sample/src/main/java/com/euonia/sample/domain/EditableObjectBase.java create mode 100644 sample/src/main/java/com/euonia/sample/domain/event/UserCreatedEvent.java diff --git a/sample/src/main/java/com/euonia/sample/domain/EditableObjectBase.java b/sample/src/main/java/com/euonia/sample/domain/EditableObjectBase.java new file mode 100644 index 0000000..ba68eb0 --- /dev/null +++ b/sample/src/main/java/com/euonia/sample/domain/EditableObjectBase.java @@ -0,0 +1,57 @@ +package com.euonia.sample.domain; + +import com.euonia.domain.Aggregate; +import com.euonia.domain.HasDomainEvents; +import com.euonia.domain.event.DomainEvent; +import com.euonia.osba.EditableObject; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; + +@Component +@Scope("prototype") +public abstract class EditableObjectBase, ID extends Comparable> extends EditableObject implements Aggregate, HasDomainEvents { + private final List events = new ArrayList<>(); + private final ConcurrentMap, List>> eventHandlers = new ConcurrentHashMap<>(); + + public void registerEvent(Class eventType, Consumer handler) { + eventHandlers.computeIfAbsent(eventType, k -> new ArrayList<>()) + .add(event -> handler.accept(eventType.cast(event))); + } + + @Override + public List getEvents() { + return List.copyOf(events); + } + + @Override + public void raiseEvent(E event) { + applyEvent(event); + events.add(event); + } + + @Override + public void applyEvent(E event) { + var handlers = eventHandlers.getOrDefault(event.getClass(), null); + if (handlers != null) { + handlers.forEach(handler -> handler.accept(event)); + } + } + + @Override + public void clearEvents() { + events.clear(); + } + + @Override + public void attachEvents() { + for (DomainEvent event : events) { + event.attach(this); + } + } +} diff --git a/sample/src/main/java/com/euonia/sample/domain/aggregate/User.java b/sample/src/main/java/com/euonia/sample/domain/aggregate/User.java index 086bf8b..92bd49c 100644 --- a/sample/src/main/java/com/euonia/sample/domain/aggregate/User.java +++ b/sample/src/main/java/com/euonia/sample/domain/aggregate/User.java @@ -2,24 +2,20 @@ import com.euonia.annotation.Required; import com.euonia.core.ObjectId; -import com.euonia.domain.Aggregate; import com.euonia.factory.annotation.FactoryCreate; -import com.euonia.osba.EditableObject; import com.euonia.osba.rules.LambdaRule; import com.euonia.osba.rules.RuleBase; import com.euonia.osba.rules.RuleContext; import com.euonia.reflection.DisplayName; import com.euonia.reflection.PropertyInfo; +import com.euonia.sample.domain.EditableObjectBase; +import com.euonia.sample.domain.event.UserCreatedEvent; import com.euonia.sample.persistent.UserRepository; -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Component; import java.util.Objects; import java.util.concurrent.CompletableFuture; -@Component -@Scope("prototype") -public class User extends EditableObject implements Aggregate { +public class User extends EditableObjectBase { private final PropertyInfo id = registerProperty(Long.class, "id"); @DisplayName("User Name") @@ -29,10 +25,12 @@ public class User extends EditableObject implements Aggregate { @DisplayName("User age") private final PropertyInfo age = registerProperty(Integer.class, "age"); + @Override public Long getId() { return getProperty(id); } + @Override public void setId(Long id) { loadProperty(this.id, id); } @@ -65,6 +63,7 @@ protected void create(String name) { super.create(); setName(name); setId(Objects.requireNonNull(ObjectId.snowflake().getValue(Long.class))); + raiseEvent(new UserCreatedEvent(getId(), name)); } @Override diff --git a/sample/src/main/java/com/euonia/sample/domain/event/UserCreatedEvent.java b/sample/src/main/java/com/euonia/sample/domain/event/UserCreatedEvent.java new file mode 100644 index 0000000..6691654 --- /dev/null +++ b/sample/src/main/java/com/euonia/sample/domain/event/UserCreatedEvent.java @@ -0,0 +1,13 @@ +package com.euonia.sample.domain.event; + +import com.euonia.domain.event.DomainEventBase; + +public class UserCreatedEvent extends DomainEventBase { + private final long id; + private final String name; + + public UserCreatedEvent(long id, String name) { + this.id = id; + this.name = name; + } +} From e86934483e55408a0d6eced6ef6b9dfc16a5d642 Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 9 Jun 2026 17:09:57 +0800 Subject: [PATCH 10/10] Refactor BaseApplicationService to introduce a generic getService method for improved service resolution --- .../java/com/euonia/application/BaseApplicationService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ddd/src/main/java/com/euonia/application/BaseApplicationService.java b/ddd/src/main/java/com/euonia/application/BaseApplicationService.java index fd46c5c..ed09c01 100644 --- a/ddd/src/main/java/com/euonia/application/BaseApplicationService.java +++ b/ddd/src/main/java/com/euonia/application/BaseApplicationService.java @@ -16,7 +16,11 @@ protected BaseApplicationService(ServiceResolver serviceResolver) { this.serviceResolver = serviceResolver; } - public UserPrincipal getUser() { + protected T getService(Class type) { + return (T) serviceResolver.getService(type); + } + + protected UserPrincipal getUser() { return serviceResolver.getService(UserPrincipal.class); } }