From ae8bf23505cc53f615a69550e42bd92094306058 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 15:36:56 +0800 Subject: [PATCH 01/16] Update README.md with comprehensive project overview, module descriptions, and quick start guide --- README.md | 192 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 42d9af2..0c099d8 100644 --- a/README.md +++ b/README.md @@ -1 +1,191 @@ -# euonia-java \ No newline at end of file +# Euonia (Java) + +> *Eunoia* — from Greek *εὔνοια*: beautiful thinking, goodwill, a well-disposed mind. + +Euonia is a development framework for building enterprise Java applications. It combines **Object-Oriented Scalable Business Architecture (OSBA)** with **Domain-Driven Design (DDD)** principles to provide a comprehensive foundation for creating robust, maintainable business applications. The framework is built on **Java 17+** and integrates seamlessly with **Spring Boot**. + +Euonia is also available for **[.NET](https://github.com/NerosoftDev/Euonia)** — this repository hosts the **Java edition**. + +--- + +## Modules + +```mermaid +graph TD + subgraph "Euonia Java" + direction TB + Domain --> Core + OSBA --> Core + Sample --> Domain + Sample --> OSBA + end + + style Core fill:#4A90D9,color:#fff + style Domain fill:#50B86C,color:#fff + style OSBA fill:#E8833A,color:#fff + style Sample fill:#9B59B6,color:#fff +``` + +### Core (`euonia-core`) +> Foundation library: base classes, ID generation, reflection utilities, tuples, HTTP exceptions, security, and validation annotations. + +| Package | Description | +|---------|-------------| +| `com.euonia.core` | Unified `ObjectId` (supports Snowflake, UUID, ULID, Random), `SnowflakeId`, `ULID`, `ShortUniqueId`, `Singleton`, `PriorityQueue`, `Pair` | +| `com.euonia.tuple` | Immutable typed tuples: `Solo`, `Duet`, `Trio`, `Quartet`, `Quintet`, `Sextet`, `Septet`, `Octet`, `Nonet`, `Decet` | +| `com.euonia.http` | HTTP status exceptions: `BadRequestException` (400), `UnauthorizedAccessException` (401), `ForbiddenException` (403), `ResourceNotFoundException` (404), `ConflictException` (409), and more | +| `com.euonia.security` | `UserPrincipal`, `UserClaimTypes`, `AuthenticationException`, `CredentialException`, `UnauthorizedAccessException` | +| `com.euonia.annotation` | `@Required`, `@Validator`, `@Validation` — metadata for field validation | +| `com.euonia.reflection` | `TypeHelper`, `GenericType`, `@DisplayName` | + +### Domain (`euonia-domain`) +> Domain-Driven Design abstractions: entities, aggregates, value objects, domain events, and auditing support. + +| Class | Purpose | +|-------|---------| +| `Entity` / `EntityBase` | Base interface and abstract class for domain entities with identity | +| `Aggregate` / `AggregateBase` | Aggregate root with domain event management (`raiseEvent`, `clearEvents`, `attachEvents`) | +| `ValueObject` | Immutable value object with reflection-based `equals`, `hashCode`, and `compareTo` | +| `DomainEvent` / `DomainEventBase` | Domain event contract with aggregate attachment and event metadata | +| `ApplicationEvent` / `ApplicationEventBase` | Application-level event base classes | +| `EventAggregate` | Event metadata wrapper: id, eventId, typeName, originator, timestamp, sequence | +| `@Audited` / `AuditRecord` / `AuditStore` | Change auditing support for domain entities | + +### OSBA (`euonia-osba`) +> **Object-Oriented Scalable Business Architecture** — a rich business object framework with rule-based validation, property change tracking, state management, and reflection-driven factories. + +#### Business Object Hierarchy + +``` +BusinessObject — Core: rules, context, property management + └── ObservableObject — Change tracking: NEW / CHANGED / DELETED state + ├── EditableObject — Savable with async rule validation + ├── ReadOnlyObject — Immutable with permission-based access + └── ExecutableObject — Template-based operation execution +``` + +#### Key Concepts + +| Concept | Description | +|---------|-------------| +| **BusinessContext** | Service locator and object factory holder; injects context and initializes rules | +| **PropertyInfo** | Typed property metadata: name, type, friendly name, default value, field reference | +| **FieldDataManager** | Per-instance reflection-based field value management | +| **Rule System** | Async rule validation with `RuleManager` (per-type singleton) & `Rules` (per-instance executor) | +| **ObjectEditState** | Lifecycle state machine: `NONE → NEW → CHANGED → DELETED` | +| **ObjectFactory** | Reflection-driven CRUD factory: `@FactoryCreate`, `@FactoryFetch`, `@FactoryInsert`, `@FactoryUpdate`, `@FactoryDelete`, `@FactoryExecute` | + +#### Rule System + +```java +protected void addRules() { + getRules().addRule(new LambdaRule<>(age, (a, ctx) -> a != null && a >= 18, "Must be 18+")); +} +``` + +| Class | Description | +|-------|-------------| +| `Rule` | Interface: `getName()`, `getProperty()`, `getPriority()`, `executeAsync(RuleContext)` | +| `LambdaRule` | Lambda-based: `(value, context) → boolean` | +| `RegularRule` | Method-based execution | +| `RequiredRule` | Non-null property validation | +| `BrokenRule` / `BrokenRuleCollection` | Validation result with severity (ERROR, WARNING, INFO) | +| `RuleCheckException` | Thrown on validation failure | + +--- + +## Sample Application + +The `sample` module demonstrates **Euonia framework integration with Spring Boot 4.0**: + +| Component | Description | +|-----------|-------------| +| **`User` aggregate** | `EditableObject` with `@FactoryCreate`, custom rules (`UserNameRule`, `LambdaRule`), and Snowflake ID generation | +| **`OsbaConfiguration`** | Wires `BusinessObjectFactory` with Spring's `ApplicationContext` | +| **`UserController`** | REST API: `POST /api/user`, `GET /api/user/{id}` — using `ObjectFactory` to create/fetch aggregates | + +### Tech Stack + +| Category | Technology | +|----------|-----------| +| **Language** | Java 17+ (sample uses Java 25) | +| **Framework** | Spring Boot 4.0 (Spring MVC, Spring Data JPA) | +| **Database** | MySQL, H2 (in-memory for testing) | +| **API Docs** | SpringDoc OpenAPI 3.0 | +| **Build** | Maven | +| **ID Generation** | Snowflake, UUID, ULID | + +--- + +## Quick Start + +```xml + + com.euonia + osba + 1.0.0 + +``` + +```java +// Define a business object +@Component @Scope("prototype") +public class Order extends EditableObject { + private final PropertyInfo productName = registerProperty(String.class, "productName"); + + @FactoryCreate + protected void create(String productName) { + super.create(); + setProductName(productName); + setId(ObjectId.snowflake().getValue(Long.class)); + } + + @Override + protected void addRules() { + getRules().addRule(new RequiredRule(productName)); + } +} + +// Use the factory +@Autowired +private ObjectFactory factory; + +var order = factory.create(Order.class, "Widget"); +order.save(false); +``` + +--- + +## Build + +```bash +# Build all modules +mvn clean install + +# Run the sample application +cd sample +mvn spring-boot:run +``` + +--- + +## Project Links + +- **GitHub**: [github.com/NerosoftDev/euonia-java](https://github.com/NerosoftDev/euonia-java) +- **.NET Edition**: [github.com/NerosoftDev/Euonia](https://github.com/NerosoftDev/Euonia) + +--- + +## Donate + +donate + +--- + +[![JetBrains](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/) + +Thanks to [JetBrains](https://www.jetbrains.com/) for supporting the project through [All Products Packs](https://www.jetbrains.com/products.html) within their [Free Open Source License](https://www.jetbrains.com/community/opensource) program. + +--- + +![Alt](https://repobeats.axiom.co/api/embed/5dc93c910fbd2dc550495a9325f7bcd0235a6082.svg "Repobeats analytics image") From 6853c98d4527bb8bc9b8ccff8902b8bf8ccbe5f9 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 15:43:10 +0800 Subject: [PATCH 02/16] Enhance documentation for Entity and ApplicationEvent classes with detailed Javadoc comments --- domain/src/main/java/com/euonia/domain/Entity.java | 5 +++++ .../main/java/com/euonia/domain/event/ApplicationEvent.java | 4 ++++ .../java/com/euonia/domain/event/ApplicationEventBase.java | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/domain/src/main/java/com/euonia/domain/Entity.java b/domain/src/main/java/com/euonia/domain/Entity.java index 22c63b5..b11b730 100644 --- a/domain/src/main/java/com/euonia/domain/Entity.java +++ b/domain/src/main/java/com/euonia/domain/Entity.java @@ -1,5 +1,10 @@ package com.euonia.domain; +/** + * The Entity interface represents a generic entity in the domain model. It defines the basic properties and methods that all entities should have. + * Entities are objects that have a distinct identity and can be distinguished from other objects based on their identity rather than their attributes. + * 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> { ID getId(); diff --git a/domain/src/main/java/com/euonia/domain/event/ApplicationEvent.java b/domain/src/main/java/com/euonia/domain/event/ApplicationEvent.java index 6591b91..7be3d49 100644 --- a/domain/src/main/java/com/euonia/domain/event/ApplicationEvent.java +++ b/domain/src/main/java/com/euonia/domain/event/ApplicationEvent.java @@ -1,4 +1,8 @@ package com.euonia.domain.event; +/** + * ApplicationEvent is a marker interface for events that are published within the application context. It extends the base Event interface and can be used to categorize events that are specific to the application's domain logic. + * By implementing this interface, events can be easily identified and handled by event listeners that are designed to process application-specific events. This allows for better organization and separation of concerns within the event-driven architecture of the application. + */ public interface ApplicationEvent extends Event { } diff --git a/domain/src/main/java/com/euonia/domain/event/ApplicationEventBase.java b/domain/src/main/java/com/euonia/domain/event/ApplicationEventBase.java index eb1fea8..121d04c 100644 --- a/domain/src/main/java/com/euonia/domain/event/ApplicationEventBase.java +++ b/domain/src/main/java/com/euonia/domain/event/ApplicationEventBase.java @@ -1,4 +1,8 @@ package com.euonia.domain.event; +/** + * ApplicationEventBase is an abstract class that serves as a base implementation for application events. It extends the EventBase class and implements the ApplicationEvent interface, providing a common foundation for all application-specific events in the domain model. + * This class can be extended by concrete event classes that represent specific events within the application, allowing for consistent handling and processing of application events while still providing the flexibility to define event-specific properties and behaviors as needed. + */ public abstract class ApplicationEventBase extends EventBase implements ApplicationEvent { } From 92b8c5f0535f9f44e0f6ef4dbfcb17f9846358a2 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 15:51:05 +0800 Subject: [PATCH 03/16] Implement GUID generation with GuidGenerator and GuidType; add tests for various GUID strategies --- .../java/com/euonia/core/GuidGenerator.java | 98 ++++++++++++++++ .../main/java/com/euonia/core/GuidType.java | 12 ++ .../main/java/com/euonia/core/ObjectId.java | 12 +- .../com/euonia/core/GuidGeneratorTest.java | 106 ++++++++++++++++++ .../java/com/euonia/core/ObjectIdTest.java | 20 +++- 5 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 core/src/main/java/com/euonia/core/GuidGenerator.java create mode 100644 core/src/main/java/com/euonia/core/GuidType.java create mode 100644 core/src/test/java/com/euonia/core/GuidGeneratorTest.java diff --git a/core/src/main/java/com/euonia/core/GuidGenerator.java b/core/src/main/java/com/euonia/core/GuidGenerator.java new file mode 100644 index 0000000..8a79e07 --- /dev/null +++ b/core/src/main/java/com/euonia/core/GuidGenerator.java @@ -0,0 +1,98 @@ +package com.euonia.core; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.UUID; + +/** + * Generates UUID values using layouts that match the framework's .NET GUID + * generation modes. + */ +public final class GuidGenerator { + + private static final long DOTNET_EPOCH_OFFSET_MILLIS = 62_135_596_800_000L; + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + private GuidGenerator() { + // utility class + } + + /** + * Creates a UUID using the specified GUID generation mode. + * + * @param type the requested GUID type + * @return generated UUID + */ + public static UUID generate(GuidType type) { + if (type == GuidType.EMPTY) { + return new UUID(0L, 0L); + } + + if (type == GuidType.SIMPLE) { + return UUID.randomUUID(); + } + + byte[] randomBytes = new byte[10]; + SECURE_RANDOM.nextBytes(randomBytes); + + byte[] timestampBytes = getTimestampBytes(); + byte[] guidBytes = new byte[16]; + + switch (type) { + case SEQUENTIAL_AS_STRING: + case SEQUENTIAL_AS_BINARY: + System.arraycopy(timestampBytes, 2, guidBytes, 0, 6); + System.arraycopy(randomBytes, 0, guidBytes, 6, 10); + + if (type == GuidType.SEQUENTIAL_AS_STRING) { + reverse(guidBytes, 0, 4); + reverse(guidBytes, 4, 2); + } + break; + + case SEQUENTIAL_AT_END: + System.arraycopy(randomBytes, 0, guidBytes, 0, 10); + System.arraycopy(timestampBytes, 2, guidBytes, 10, 6); + break; + + default: + throw new IllegalArgumentException("Unsupported GuidType: " + type); + } + + return fromDotNetBytes(guidBytes); + } + + private static byte[] getTimestampBytes() { + long timestamp = System.currentTimeMillis() + DOTNET_EPOCH_OFFSET_MILLIS; + byte[] bytes = new byte[8]; + bytes[0] = (byte) (timestamp >> 56); + bytes[1] = (byte) (timestamp >> 48); + bytes[2] = (byte) (timestamp >> 40); + bytes[3] = (byte) (timestamp >> 32); + bytes[4] = (byte) (timestamp >> 24); + bytes[5] = (byte) (timestamp >> 16); + bytes[6] = (byte) (timestamp >> 8); + bytes[7] = (byte) timestamp; + return bytes; + } + + private static UUID fromDotNetBytes(byte[] dotNetBytes) { + byte[] uuidBytes = dotNetBytes.clone(); + reverse(uuidBytes, 0, 4); + reverse(uuidBytes, 4, 2); + reverse(uuidBytes, 6, 2); + + ByteBuffer buffer = ByteBuffer.wrap(uuidBytes); + long mostSignificantBits = buffer.getLong(); + long leastSignificantBits = buffer.getLong(); + return new UUID(mostSignificantBits, leastSignificantBits); + } + + private static void reverse(byte[] bytes, int offset, int length) { + for (int left = offset, right = offset + length - 1; left < right; left++, right--) { + byte current = bytes[left]; + bytes[left] = bytes[right]; + bytes[right] = current; + } + } +} diff --git a/core/src/main/java/com/euonia/core/GuidType.java b/core/src/main/java/com/euonia/core/GuidType.java new file mode 100644 index 0000000..e35b8ba --- /dev/null +++ b/core/src/main/java/com/euonia/core/GuidType.java @@ -0,0 +1,12 @@ +package com.euonia.core; + +/** + * Describes the type of GUID value to generate. + */ +public enum GuidType { + EMPTY, + SIMPLE, + SEQUENTIAL_AS_STRING, + SEQUENTIAL_AS_BINARY, + SEQUENTIAL_AT_END +} diff --git a/core/src/main/java/com/euonia/core/ObjectId.java b/core/src/main/java/com/euonia/core/ObjectId.java index e656df4..6587843 100644 --- a/core/src/main/java/com/euonia/core/ObjectId.java +++ b/core/src/main/java/com/euonia/core/ObjectId.java @@ -78,7 +78,17 @@ public static ObjectId snowflake() { * @return a new ObjectId */ public static ObjectId guid() { - return new ObjectId(UUID.randomUUID()); + return guid(GuidType.SIMPLE); + } + + /** + * Generates a new ObjectId using the GUID algorithm. + * + * @param type the GUID generation strategy + * @return a new ObjectId + */ + public static ObjectId guid(GuidType type) { + return new ObjectId(GuidGenerator.generate(type)); } /** diff --git a/core/src/test/java/com/euonia/core/GuidGeneratorTest.java b/core/src/test/java/com/euonia/core/GuidGeneratorTest.java new file mode 100644 index 0000000..0c28d32 --- /dev/null +++ b/core/src/test/java/com/euonia/core/GuidGeneratorTest.java @@ -0,0 +1,106 @@ +package com.euonia.core; + +import java.nio.ByteBuffer; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("GuidGenerator") +class GuidGeneratorTest { + + private static final long DOTNET_EPOCH_OFFSET_MILLIS = 62_135_596_800_000L; + + @Test + @DisplayName("Given empty type when generating then all-zero UUID is returned") + void givenEmptyTypeWhenGeneratingThenReturnEmptyUuid() { + UUID guid = GuidGenerator.generate(GuidType.EMPTY); + + assertEquals(new UUID(0L, 0L), guid); + } + + @Test + @DisplayName("Given simple type when generating then non-empty UUID is returned") + void givenSimpleTypeWhenGeneratingThenReturnNonEmptyUuid() { + UUID guid = GuidGenerator.generate(GuidType.SIMPLE); + + assertNotEquals(new UUID(0L, 0L), guid); + } + + @Test + @DisplayName("Given sequential-as-string type when generating then timestamp is encoded at canonical start") + void givenSequentialAsStringTypeWhenGeneratingThenTimestampIsAtCanonicalStart() { + long before = dotNetNowMillis(); + UUID guid = GuidGenerator.generate(GuidType.SEQUENTIAL_AS_STRING); + long after = dotNetNowMillis(); + + long embeddedTimestamp = decode48BitTimestamp(uuidBytes(guid), 0); + + assertTrue(embeddedTimestamp >= before); + assertTrue(embeddedTimestamp <= after); + } + + @Test + @DisplayName("Given sequential-as-binary type when generating then timestamp is encoded at dotnet byte-array start") + void givenSequentialAsBinaryTypeWhenGeneratingThenTimestampIsAtDotNetByteArrayStart() { + long before = dotNetNowMillis(); + UUID guid = GuidGenerator.generate(GuidType.SEQUENTIAL_AS_BINARY); + long after = dotNetNowMillis(); + + long embeddedTimestamp = decode48BitTimestamp(toDotNetBytes(guid), 0); + + assertTrue(embeddedTimestamp >= before); + assertTrue(embeddedTimestamp <= after); + } + + @Test + @DisplayName("Given sequential-at-end type when generating then timestamp is encoded at dotnet byte-array end") + void givenSequentialAtEndTypeWhenGeneratingThenTimestampIsAtDotNetByteArrayEnd() { + long before = dotNetNowMillis(); + UUID guid = GuidGenerator.generate(GuidType.SEQUENTIAL_AT_END); + long after = dotNetNowMillis(); + + long embeddedTimestamp = decode48BitTimestamp(toDotNetBytes(guid), 10); + + assertTrue(embeddedTimestamp >= before); + assertTrue(embeddedTimestamp <= after); + } + + private static long dotNetNowMillis() { + return System.currentTimeMillis() + DOTNET_EPOCH_OFFSET_MILLIS; + } + + private static byte[] uuidBytes(UUID value) { + ByteBuffer buffer = ByteBuffer.allocate(16); + buffer.putLong(value.getMostSignificantBits()); + buffer.putLong(value.getLeastSignificantBits()); + return buffer.array(); + } + + private static byte[] toDotNetBytes(UUID value) { + byte[] bytes = uuidBytes(value); + reverse(bytes, 0, 4); + reverse(bytes, 4, 2); + reverse(bytes, 6, 2); + return bytes; + } + + private static long decode48BitTimestamp(byte[] bytes, int offset) { + long value = 0L; + for (int index = offset; index < offset + 6; index++) { + value = (value << 8) | (bytes[index] & 0xFFL); + } + return value; + } + + private static void reverse(byte[] bytes, int offset, int length) { + for (int left = offset, right = offset + length - 1; left < right; left++, right--) { + byte current = bytes[left]; + bytes[left] = bytes[right]; + bytes[right] = current; + } + } +} diff --git a/core/src/test/java/com/euonia/core/ObjectIdTest.java b/core/src/test/java/com/euonia/core/ObjectIdTest.java index adea74b..cac3ff4 100644 --- a/core/src/test/java/com/euonia/core/ObjectIdTest.java +++ b/core/src/test/java/com/euonia/core/ObjectIdTest.java @@ -1,11 +1,14 @@ package com.euonia.core; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - import java.util.UUID; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; @DisplayName("ObjectId") class ObjectIdTest { @@ -119,5 +122,14 @@ void givenGuidAndRandomFactoriesWhenGeneratingThenReturnUuidValues() { assertDoesNotThrow(() -> UUID.fromString(guid.toString())); assertDoesNotThrow(() -> UUID.fromString(random.toString())); } + + @Test + @DisplayName("Given GUID type when generating then typed GUID strategy is supported") + void givenGuidTypeWhenGeneratingThenTypedGuidStrategyIsSupported() { + ObjectId guid = ObjectId.guid(GuidType.SEQUENTIAL_AS_STRING); + + assertInstanceOf(UUID.class, guid.getValue()); + assertDoesNotThrow(() -> UUID.fromString(guid.toString())); + } } From 491cddece697067ed39d0573f5e3ae542f5f7d34 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 16:52:12 +0800 Subject: [PATCH 04/16] Add ServiceResolver interface and SimpleServiceResolver implementation for service management --- .../euonia/reflection/ServiceResolver.java | 14 ++++ .../reflection/SimpleServiceResolver.java | 78 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 core/src/main/java/com/euonia/reflection/ServiceResolver.java create mode 100644 core/src/main/java/com/euonia/reflection/SimpleServiceResolver.java diff --git a/core/src/main/java/com/euonia/reflection/ServiceResolver.java b/core/src/main/java/com/euonia/reflection/ServiceResolver.java new file mode 100644 index 0000000..d0a01a2 --- /dev/null +++ b/core/src/main/java/com/euonia/reflection/ServiceResolver.java @@ -0,0 +1,14 @@ +package com.euonia.reflection; + +public interface ServiceResolver { + T getService(Class type); + + T getRequiredService(Class type); + + T createInstance(Class type, Object... constructorArguments); + + default T getServiceOrCreate(Class type, Object... constructorArguments) { + T service = getService(type); + return service != null ? service : createInstance(type, constructorArguments); + } +} diff --git a/core/src/main/java/com/euonia/reflection/SimpleServiceResolver.java b/core/src/main/java/com/euonia/reflection/SimpleServiceResolver.java new file mode 100644 index 0000000..d6cd912 --- /dev/null +++ b/core/src/main/java/com/euonia/reflection/SimpleServiceResolver.java @@ -0,0 +1,78 @@ +package com.euonia.reflection; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class SimpleServiceResolver implements ServiceResolver { + private final Map, Object> services = new ConcurrentHashMap<>(); + + public void register(Class type, T instance) { + services.put(type, instance); + } + + @Override + @SuppressWarnings("unchecked") + public T getService(Class type) { + Object service = services.get(type); + if (service == null) { + return null; + } + return (T) service; + } + + @Override + public T getRequiredService(Class type) { + T service = getService(type); + if (service == null) { + throw new IllegalStateException("Can not resolve service: " + type.getName()); + } + return service; + } + + @Override + public T createInstance(Class type, Object... constructorArguments) { + Constructor[] constructors = type.getDeclaredConstructors(); + for (Constructor constructor : constructors) { + Class[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length != constructorArguments.length) { + continue; + } + + if (!isAssignable(parameterTypes, constructorArguments)) { + continue; + } + + try { + constructor.setAccessible(true); + @SuppressWarnings("unchecked") + T created = (T) constructor.newInstance(constructorArguments); + return created; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException("Could not construct type: " + type.getName(), e); + } + } + + try { + Constructor constructor = type.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IllegalStateException("Could not construct type: " + type.getName(), e); + } + } + + private static boolean isAssignable(Class[] parameterTypes, Object[] args) { + for (int i = 0; i < parameterTypes.length; i++) { + Object arg = args[i]; + if (arg == null) { + continue; + } + if (!parameterTypes[i].isAssignableFrom(arg.getClass())) { + return false; + } + } + return true; + } +} From ded4277c530ee57f147330f594bab4d43922af7d Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 16:52:45 +0800 Subject: [PATCH 05/16] Add name element for the euonia domain module in pom.xml --- domain/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/domain/pom.xml b/domain/pom.xml index e6d65d0..c4dfd3e 100644 --- a/domain/pom.xml +++ b/domain/pom.xml @@ -10,6 +10,7 @@ domain + euonia domain module From f3bae22472ae1e6201c4eaa8640831a9401aa7d3 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 16:53:01 +0800 Subject: [PATCH 06/16] Implement pipeline architecture with core components and behaviors --- pipeline/README.md | 12 +++ pipeline/pom.xml | 40 +++++++++ .../pipeline/DefaultPipelineFactory.java | 21 +++++ .../pipeline/DefaultPipelineProvider.java | 86 ++++++++++++++++++ ...efaultRequestResponsePipelineProvider.java | 90 +++++++++++++++++++ .../java/com/euonia/pipeline/Pipeline.java | 23 +++++ .../com/euonia/pipeline/PipelineBase.java | 85 ++++++++++++++++++ .../com/euonia/pipeline/PipelineBehavior.java | 7 ++ .../euonia/pipeline/PipelineBehaviorType.java | 14 +++ .../euonia/pipeline/PipelineBehaviors.java | 12 +++ .../com/euonia/pipeline/PipelineDelegate.java | 8 ++ .../com/euonia/pipeline/PipelineFactory.java | 7 ++ .../pipeline/RequestPipelineDelegate.java | 8 ++ .../pipeline/RequestResponsePipeline.java | 24 +++++ .../pipeline/RequestResponsePipelineBase.java | 89 ++++++++++++++++++ .../RequestResponsePipelineBehavior.java | 7 ++ .../RequestResponsePipelineDelegate.java | 8 ++ .../DefaultPipelineProviderTests.java | 72 +++++++++++++++ ...tRequestResponsePipelineProviderTests.java | 70 +++++++++++++++ .../PipelineSpringIntegrationTests.java | 65 ++++++++++++++ 20 files changed, 748 insertions(+) create mode 100644 pipeline/README.md create mode 100644 pipeline/pom.xml create mode 100644 pipeline/src/main/java/com/euonia/pipeline/DefaultPipelineFactory.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/DefaultPipelineProvider.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/DefaultRequestResponsePipelineProvider.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/Pipeline.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/PipelineBase.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/PipelineBehavior.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviorType.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviors.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/PipelineDelegate.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/PipelineFactory.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/RequestPipelineDelegate.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipeline.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineBase.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineBehavior.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineDelegate.java create mode 100644 pipeline/src/test/java/com/euonia/pipeline/DefaultPipelineProviderTests.java create mode 100644 pipeline/src/test/java/com/euonia/pipeline/DefaultRequestResponsePipelineProviderTests.java create mode 100644 pipeline/src/test/java/com/euonia/pipeline/spring/PipelineSpringIntegrationTests.java diff --git a/pipeline/README.md b/pipeline/README.md new file mode 100644 index 0000000..27bc7cc --- /dev/null +++ b/pipeline/README.md @@ -0,0 +1,12 @@ + +# Configure Pipeline + +``` java +@Configuration +public class PipelineConfiguration { + @Bean + public PipelineFactory pipelineFactory(ServiceResolver resolver) { + return new DefaultPipelineFactory(resolver); + } +} +``` diff --git a/pipeline/pom.xml b/pipeline/pom.xml new file mode 100644 index 0000000..f71708c --- /dev/null +++ b/pipeline/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + com.euonia + parent + ${revision} + + + pipeline + euonia pipeline module + + + + com.euonia + core + ${revision} + + + + org.junit.jupiter + junit-jupiter + 5.12.2 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.3 + + + + + diff --git a/pipeline/src/main/java/com/euonia/pipeline/DefaultPipelineFactory.java b/pipeline/src/main/java/com/euonia/pipeline/DefaultPipelineFactory.java new file mode 100644 index 0000000..db27b34 --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/DefaultPipelineFactory.java @@ -0,0 +1,21 @@ +package com.euonia.pipeline; + +import com.euonia.reflection.ServiceResolver; + +public class DefaultPipelineFactory implements PipelineFactory { + private final ServiceResolver resolver; + + public DefaultPipelineFactory(ServiceResolver resolver) { + this.resolver = resolver; + } + + @Override + public Pipeline create() { + return new DefaultPipelineProvider(resolver); + } + + @Override + public RequestResponsePipeline createRequestResponse() { + return new DefaultRequestResponsePipelineProvider<>(resolver); + } +} diff --git a/pipeline/src/main/java/com/euonia/pipeline/DefaultPipelineProvider.java b/pipeline/src/main/java/com/euonia/pipeline/DefaultPipelineProvider.java new file mode 100644 index 0000000..c1ecdbc --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/DefaultPipelineProvider.java @@ -0,0 +1,86 @@ +package com.euonia.pipeline; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Comparator; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import com.euonia.reflection.ServiceResolver; + +public class DefaultPipelineProvider extends PipelineBase { + private static final String HANDLE_METHOD_NAME = "handle"; + private static final String HANDLE_METHOD_NAME_ASYNC = "handleAsync"; + + private final ServiceResolver resolver; + + public DefaultPipelineProvider(ServiceResolver resolver) { + this.resolver = resolver; + } + + @Override + protected PipelineDelegate getNext(PipelineDelegate next, Class behaviorType, Object... constructorArguments) { + if (PipelineBehavior.class.isAssignableFrom(behaviorType)) { + return context -> { + PipelineBehavior behavior = resolver.getServiceOrCreate(castType(behaviorType), constructorArguments); + return behavior.handleAsync(context, next); + }; + } + + Method method = resolveHandleMethod(behaviorType); + Object[] ctorArgs = prepend(next, constructorArguments); + Object instance = resolver.createInstance(castType(behaviorType), ctorArgs); + + return context -> invokeVoidLike(method, instance, context, resolver); + } + + @SuppressWarnings("unchecked") + private static Class castType(Class type) { + return (Class) type; + } + + private static CompletionStage invokeVoidLike(Method method, Object instance, Object context, ServiceResolver resolver) { + try { + Object[] args = new Object[method.getParameterCount()]; + args[0] = context; + Class[] parameterTypes = method.getParameterTypes(); + for (int i = 1; i < parameterTypes.length; i++) { + args[i] = resolver.getRequiredService(parameterTypes[i]); + } + Object value = method.invoke(instance, args); + if (value instanceof CompletionStage stage) { + return stage.thenApply(ignored -> null); + } + return CompletableFuture.completedFuture(null); + } catch (Exception ex) { + return CompletableFuture.failedFuture(ex); + } + } + + private static Method resolveHandleMethod(Class behaviorType) { + Method[] candidates = Arrays.stream(behaviorType.getMethods()) + .filter(m -> HANDLE_METHOD_NAME.equals(m.getName()) || HANDLE_METHOD_NAME_ASYNC.equals(m.getName())) + .filter(m -> m.getParameterCount() >= 1) + .filter(m -> CompletionStage.class.isAssignableFrom(m.getReturnType())) + .sorted(Comparator.comparingInt(Method::getParameterCount)) + .toArray(Method[]::new); + + if (candidates.length == 0) { + throw new IllegalStateException("Method handle/handleAsync not found on " + behaviorType.getName()); + } + if (candidates.length > 1) { + throw new IllegalStateException("Multiple handle methods found on " + behaviorType.getName()); + } + + Method method = candidates[0]; + method.setAccessible(true); + return method; + } + + private static Object[] prepend(Object first, Object[] rest) { + Object[] values = new Object[rest.length + 1]; + values[0] = first; + System.arraycopy(rest, 0, values, 1, rest.length); + return values; + } +} diff --git a/pipeline/src/main/java/com/euonia/pipeline/DefaultRequestResponsePipelineProvider.java b/pipeline/src/main/java/com/euonia/pipeline/DefaultRequestResponsePipelineProvider.java new file mode 100644 index 0000000..6271b7c --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/DefaultRequestResponsePipelineProvider.java @@ -0,0 +1,90 @@ +package com.euonia.pipeline; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Comparator; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import com.euonia.reflection.ServiceResolver; + +public class DefaultRequestResponsePipelineProvider extends RequestResponsePipelineBase { + private static final String HANDLE_METHOD_NAME = "handle"; + private static final String HANDLE_METHOD_NAME_ASYNC = "handleAsync"; + + private final ServiceResolver resolver; + + public DefaultRequestResponsePipelineProvider(ServiceResolver resolver) { + this.resolver = resolver; + } + + @Override + protected RequestResponsePipelineDelegate getNext(RequestResponsePipelineDelegate next, + Class type, + Object... constructorArguments) { + if (RequestResponsePipelineBehavior.class.isAssignableFrom(type)) { + return request -> { + RequestResponsePipelineBehavior behavior = resolver.getServiceOrCreate(castType(type), constructorArguments); + return behavior.handleAsync(request, next); + }; + } + + Method method = resolveHandleMethod(type); + Object[] ctorArgs = prepend(next, constructorArguments); + Object instance = resolver.createInstance(castType(type), ctorArgs); + + return request -> invokeTyped(method, instance, request, resolver); + } + + @SuppressWarnings("unchecked") + private static Class castType(Class type) { + return (Class) type; + } + + @SuppressWarnings("unchecked") + private CompletionStage invokeTyped(Method method, Object instance, TRequest request, ServiceResolver resolver) { + try { + Object[] args = new Object[method.getParameterCount()]; + args[0] = request; + Class[] parameterTypes = method.getParameterTypes(); + for (int i = 1; i < parameterTypes.length; i++) { + args[i] = resolver.getRequiredService(parameterTypes[i]); + } + Object value = method.invoke(instance, args); + if (!(value instanceof CompletionStage stage)) { + return CompletableFuture.failedFuture(new IllegalStateException("Handle method must return CompletionStage.")); + } + return (CompletionStage) stage; + } catch (IllegalAccessException | InvocationTargetException ex) { + return CompletableFuture.failedFuture(ex); + } + } + + private static Method resolveHandleMethod(Class behaviorType) { + Method[] candidates = Arrays.stream(behaviorType.getMethods()) + .filter(m -> HANDLE_METHOD_NAME.equals(m.getName()) || HANDLE_METHOD_NAME_ASYNC.equals(m.getName())) + .filter(m -> m.getParameterCount() >= 1) + .filter(m -> CompletionStage.class.isAssignableFrom(m.getReturnType())) + .sorted(Comparator.comparingInt(Method::getParameterCount)) + .toArray(Method[]::new); + + if (candidates.length == 0) { + throw new IllegalStateException("Method handle/handleAsync not found on " + behaviorType.getName()); + } + if (candidates.length > 1) { + throw new IllegalStateException("Multiple handle methods found on " + behaviorType.getName()); + } + + Method method = candidates[0]; + method.setAccessible(true); + return method; + } + + private static Object[] prepend(Object first, Object[] rest) { + Object[] values = new Object[rest.length + 1]; + values[0] = first; + System.arraycopy(rest, 0, values, 1, rest.length); + return values; + } +} diff --git a/pipeline/src/main/java/com/euonia/pipeline/Pipeline.java b/pipeline/src/main/java/com/euonia/pipeline/Pipeline.java new file mode 100644 index 0000000..92cdb01 --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/Pipeline.java @@ -0,0 +1,23 @@ +package com.euonia.pipeline; + +import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; +import java.util.function.Function; + +public interface Pipeline { + Pipeline use(Function component); + + Pipeline use(Function component, int index); + + Pipeline use(BiFunction> handler); + + Pipeline use(Class type, Object... args); + + Pipeline useOf(Class contextType, boolean useAheadOfOthers); + + PipelineDelegate build(); + + CompletionStage runAsync(Object context); + + CompletionStage runAsync(Object context, Function> accumulate); +} diff --git a/pipeline/src/main/java/com/euonia/pipeline/PipelineBase.java b/pipeline/src/main/java/com/euonia/pipeline/PipelineBase.java new file mode 100644 index 0000000..bc51d78 --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/PipelineBase.java @@ -0,0 +1,85 @@ +package com.euonia.pipeline; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; +import java.util.function.Function; + +public abstract class PipelineBase implements Pipeline { + private final List> components = new ArrayList<>(); + + public List> getComponents() { + return List.copyOf(components); + } + + @Override + public Pipeline use(Function component) { + components.add(component); + return this; + } + + @Override + public Pipeline use(Function component, int index) { + components.add(index, component); + return this; + } + + @Override + public Pipeline use(BiFunction> handler) { + return use(next -> context -> handler.apply(context, next)); + } + + @Override + public Pipeline use(Class type, Object... args) { + return use(next -> getNext(next, type, args)); + } + + @Override + public Pipeline useOf(Class contextType, boolean useAheadOfOthers) { + PipelineBehaviorType[] attributes = contextType.getAnnotationsByType(PipelineBehaviorType.class); + if (useAheadOfOthers) { + for (int index = 0; index < attributes.length; index++) { + Class behaviorType = attributes[index].behaviorType(); + use(next -> getNext(next, behaviorType), index); + } + return this; + } + + for (PipelineBehaviorType attribute : attributes) { + use(attribute.behaviorType()); + } + return this; + } + + @Override + public PipelineDelegate build() { + try { + PipelineDelegate app = context -> CompletableFuture.completedFuture(null); + List> reversed = new ArrayList<>(components); + Collections.reverse(reversed); + for (Function component : reversed) { + app = component.apply(app); + } + return app; + } finally { + components.clear(); + } + } + + @Override + public CompletionStage runAsync(Object context) { + useOf(context.getClass(), true); + return build().invoke(context); + } + + @Override + public CompletionStage runAsync(Object context, Function> accumulate) { + use((request, next) -> accumulate.apply(request)); + return runAsync(context); + } + + protected abstract PipelineDelegate getNext(PipelineDelegate next, Class type, Object... constructorArguments); +} diff --git a/pipeline/src/main/java/com/euonia/pipeline/PipelineBehavior.java b/pipeline/src/main/java/com/euonia/pipeline/PipelineBehavior.java new file mode 100644 index 0000000..8de8633 --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/PipelineBehavior.java @@ -0,0 +1,7 @@ +package com.euonia.pipeline; + +import java.util.concurrent.CompletionStage; + +public interface PipelineBehavior { + CompletionStage handleAsync(Object context, PipelineDelegate next); +} diff --git a/pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviorType.java b/pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviorType.java new file mode 100644 index 0000000..f3a4dc9 --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviorType.java @@ -0,0 +1,14 @@ +package com.euonia.pipeline; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Repeatable(PipelineBehaviors.class) +public @interface PipelineBehaviorType { + Class behaviorType(); +} diff --git a/pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviors.java b/pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviors.java new file mode 100644 index 0000000..ad23442 --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviors.java @@ -0,0 +1,12 @@ +package com.euonia.pipeline; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface PipelineBehaviors { + PipelineBehaviorType[] value(); +} diff --git a/pipeline/src/main/java/com/euonia/pipeline/PipelineDelegate.java b/pipeline/src/main/java/com/euonia/pipeline/PipelineDelegate.java new file mode 100644 index 0000000..91bfc7a --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/PipelineDelegate.java @@ -0,0 +1,8 @@ +package com.euonia.pipeline; + +import java.util.concurrent.CompletionStage; + +@FunctionalInterface +public interface PipelineDelegate { + CompletionStage invoke(Object context); +} diff --git a/pipeline/src/main/java/com/euonia/pipeline/PipelineFactory.java b/pipeline/src/main/java/com/euonia/pipeline/PipelineFactory.java new file mode 100644 index 0000000..e513839 --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/PipelineFactory.java @@ -0,0 +1,7 @@ +package com.euonia.pipeline; + +public interface PipelineFactory { + Pipeline create(); + + RequestResponsePipeline createRequestResponse(); +} diff --git a/pipeline/src/main/java/com/euonia/pipeline/RequestPipelineDelegate.java b/pipeline/src/main/java/com/euonia/pipeline/RequestPipelineDelegate.java new file mode 100644 index 0000000..2a4c94c --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/RequestPipelineDelegate.java @@ -0,0 +1,8 @@ +package com.euonia.pipeline; + +import java.util.concurrent.CompletionStage; + +@FunctionalInterface +public interface RequestPipelineDelegate { + CompletionStage invoke(TRequest request); +} diff --git a/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipeline.java b/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipeline.java new file mode 100644 index 0000000..22bc5fe --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipeline.java @@ -0,0 +1,24 @@ +package com.euonia.pipeline; + +import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; +import java.util.function.Function; + +public interface RequestResponsePipeline { + RequestResponsePipeline use(Function, RequestResponsePipelineDelegate> component); + + RequestResponsePipeline use(Function, RequestResponsePipelineDelegate> component, + int index); + + RequestResponsePipeline use(BiFunction, CompletionStage> handler); + + RequestResponsePipeline use(Class type, Object... args); + + RequestResponsePipeline useOf(Class contextType, boolean useAheadOfOthers); + + RequestResponsePipelineDelegate build(); + + CompletionStage runAsync(TRequest context); + + CompletionStage runAsync(TRequest context, Function> accumulate); +} diff --git a/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineBase.java b/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineBase.java new file mode 100644 index 0000000..840c856 --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineBase.java @@ -0,0 +1,89 @@ +package com.euonia.pipeline; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; +import java.util.function.Function; + +public abstract class RequestResponsePipelineBase implements RequestResponsePipeline { + private final List, RequestResponsePipelineDelegate>> components = new ArrayList<>(); + + public List, RequestResponsePipelineDelegate>> getComponents() { + return List.copyOf(components); + } + + @Override + public RequestResponsePipeline use(Function, RequestResponsePipelineDelegate> component) { + components.add(component); + return this; + } + + @Override + public RequestResponsePipeline use(Function, RequestResponsePipelineDelegate> component, + int index) { + components.add(index, component); + return this; + } + + @Override + public RequestResponsePipeline use(BiFunction, CompletionStage> handler) { + return use(next -> request -> handler.apply(request, next)); + } + + @Override + public RequestResponsePipeline use(Class type, Object... args) { + return use(next -> getNext(next, type, args)); + } + + @Override + public RequestResponsePipeline useOf(Class contextType, boolean useAheadOfOthers) { + PipelineBehaviorType[] attributes = contextType.getAnnotationsByType(PipelineBehaviorType.class); + if (useAheadOfOthers) { + for (int index = 0; index < attributes.length; index++) { + Class behaviorType = attributes[index].behaviorType(); + use(next -> getNext(next, behaviorType), index); + } + return this; + } + + for (PipelineBehaviorType attribute : attributes) { + use(attribute.behaviorType()); + } + return this; + } + + @Override + public RequestResponsePipelineDelegate build() { + try { + RequestResponsePipelineDelegate app = request -> { + throw new IllegalStateException("Pipeline terminal handler is not configured."); + }; + List, RequestResponsePipelineDelegate>> reversed = new ArrayList<>(components); + Collections.reverse(reversed); + for (Function, RequestResponsePipelineDelegate> component : reversed) { + app = component.apply(app); + } + return app; + } finally { + components.clear(); + } + } + + @Override + public CompletionStage runAsync(TRequest context) { + useOf(context.getClass(), true); + return build().invoke(context); + } + + @Override + public CompletionStage runAsync(TRequest context, Function> accumulate) { + use((request, next) -> accumulate.apply(request)); + return runAsync(context); + } + + protected abstract RequestResponsePipelineDelegate getNext(RequestResponsePipelineDelegate next, + Class type, + Object... constructorArguments); +} diff --git a/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineBehavior.java b/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineBehavior.java new file mode 100644 index 0000000..da2ea5d --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineBehavior.java @@ -0,0 +1,7 @@ +package com.euonia.pipeline; + +import java.util.concurrent.CompletionStage; + +public interface RequestResponsePipelineBehavior { + CompletionStage handleAsync(TRequest context, RequestResponsePipelineDelegate next); +} diff --git a/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineDelegate.java b/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineDelegate.java new file mode 100644 index 0000000..6a935a8 --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineDelegate.java @@ -0,0 +1,8 @@ +package com.euonia.pipeline; + +import java.util.concurrent.CompletionStage; + +@FunctionalInterface +public interface RequestResponsePipelineDelegate { + CompletionStage invoke(TRequest request); +} diff --git a/pipeline/src/test/java/com/euonia/pipeline/DefaultPipelineProviderTests.java b/pipeline/src/test/java/com/euonia/pipeline/DefaultPipelineProviderTests.java new file mode 100644 index 0000000..edf1cae --- /dev/null +++ b/pipeline/src/test/java/com/euonia/pipeline/DefaultPipelineProviderTests.java @@ -0,0 +1,72 @@ +package com.euonia.pipeline; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; + +import com.euonia.reflection.SimpleServiceResolver; + +class DefaultPipelineProviderTests { + + @Test + void should_run_attribute_behavior_before_accumulate() { + DefaultPipelineProvider pipeline = new DefaultPipelineProvider(new SimpleServiceResolver()); + TraceContext context = new TraceContext(); + + pipeline.runAsync(context, ignored -> { + context.trace.add("accumulate"); + return CompletableFuture.completedFuture(null); + }).toCompletableFuture().join(); + + assertEquals(List.of("behavior", "accumulate"), context.trace); + } + + @Test + void should_resolve_method_parameters_from_service_resolver() { + SimpleServiceResolver resolver = new SimpleServiceResolver(); + MarkerService markerService = new MarkerService(); + resolver.register(MarkerService.class, markerService); + + DefaultPipelineProvider pipeline = new DefaultPipelineProvider(resolver); + pipeline.use(ReflectionPipelineBehavior.class); + + pipeline.runAsync("ctx").toCompletableFuture().join(); + + assertEquals("ctx", markerService.value); + } + + @PipelineBehaviorType(behaviorType = AnnotatedPipelineBehavior.class) + static class TraceContext { + final List trace = new ArrayList<>(); + } + + static class AnnotatedPipelineBehavior implements PipelineBehavior { + @Override + public CompletionStage handleAsync(Object context, PipelineDelegate next) { + TraceContext traceContext = (TraceContext) context; + traceContext.trace.add("behavior"); + return next.invoke(context); + } + } + + static class MarkerService { + private String value; + } + + static class ReflectionPipelineBehavior { + private final PipelineDelegate next; + + ReflectionPipelineBehavior(PipelineDelegate next) { + this.next = next; + } + + public CompletionStage handleAsync(Object context, MarkerService markerService) { + markerService.value = String.valueOf(context); + return next.invoke(context); + } + } +} diff --git a/pipeline/src/test/java/com/euonia/pipeline/DefaultRequestResponsePipelineProviderTests.java b/pipeline/src/test/java/com/euonia/pipeline/DefaultRequestResponsePipelineProviderTests.java new file mode 100644 index 0000000..e82836d --- /dev/null +++ b/pipeline/src/test/java/com/euonia/pipeline/DefaultRequestResponsePipelineProviderTests.java @@ -0,0 +1,70 @@ +package com.euonia.pipeline; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; + +import com.euonia.reflection.SimpleServiceResolver; + +class DefaultRequestResponsePipelineProviderTests { + + @Test + void should_wrap_accumulate_with_behavior() { + DefaultRequestResponsePipelineProvider pipeline = + new DefaultRequestResponsePipelineProvider<>(new SimpleServiceResolver()); + + pipeline.use(PlusOneBehavior.class); + + int result = pipeline.runAsync(2, request -> CompletableFuture.completedFuture(request * 2)) + .toCompletableFuture() + .join(); + + assertEquals(5, result); + } + + @Test + void should_resolve_dependency_parameter_for_reflection_handler() { + SimpleServiceResolver resolver = new SimpleServiceResolver(); + SuffixService suffixService = new SuffixService("-ok"); + resolver.register(SuffixService.class, suffixService); + + DefaultRequestResponsePipelineProvider pipeline = + new DefaultRequestResponsePipelineProvider<>(resolver); + pipeline.use(ReflectionRequestResponseBehavior.class); + + String value = pipeline.runAsync("input", CompletableFuture::completedFuture) + .toCompletableFuture() + .join(); + + assertEquals("input-ok", value); + } + + static class PlusOneBehavior implements RequestResponsePipelineBehavior { + @Override + public CompletionStage handleAsync(Integer context, RequestResponsePipelineDelegate next) { + return next.invoke(context).thenApply(value -> value + 1); + } + } + + static class SuffixService { + private final String suffix; + + SuffixService(String suffix) { + this.suffix = suffix; + } + } + + static class ReflectionRequestResponseBehavior { + private final RequestResponsePipelineDelegate next; + + ReflectionRequestResponseBehavior(RequestResponsePipelineDelegate next) { + this.next = next; + } + + public CompletionStage handleAsync(String context, SuffixService suffixService) { + return next.invoke(context).thenApply(value -> value + suffixService.suffix); + } + } +} diff --git a/pipeline/src/test/java/com/euonia/pipeline/spring/PipelineSpringIntegrationTests.java b/pipeline/src/test/java/com/euonia/pipeline/spring/PipelineSpringIntegrationTests.java new file mode 100644 index 0000000..30fc848 --- /dev/null +++ b/pipeline/src/test/java/com/euonia/pipeline/spring/PipelineSpringIntegrationTests.java @@ -0,0 +1,65 @@ +/* +package com.euonia.pipeline.spring; + +import java.util.concurrent.CompletionStage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import com.euonia.pipeline.Pipeline; +import com.euonia.pipeline.PipelineDelegate; +import com.euonia.pipeline.PipelineFactory; +import com.euonia.reflection.ServiceResolver; + +class PipelineSpringIntegrationTests { + + @Test + void should_create_pipeline_through_spring_configuration() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class)) { + PipelineFactory factory = context.getBean(PipelineFactory.class); + ServiceResolver resolver = context.getBean(ServiceResolver.class); + + assertNotNull(factory); + assertNotNull(resolver); + + PrefixService prefixService = context.getBean(PrefixService.class); + Pipeline pipeline = factory.create(); + pipeline.use(SpringReflectionBehavior.class); + pipeline.runAsync("value").toCompletableFuture().join(); + + assertEquals("prefix-value", prefixService.recorded); + } + } + + @Configuration + @Import(PipelineConfiguration.class) + static class TestConfiguration { + @Bean + PrefixService prefixService() { + return new PrefixService(); + } + } + + static class PrefixService { + private String recorded; + } + + static class SpringReflectionBehavior { + private final PipelineDelegate next; + + SpringReflectionBehavior(PipelineDelegate next) { + this.next = next; + } + + public CompletionStage handleAsync(Object context, PrefixService prefixService) { + prefixService.recorded = "prefix-" + context; + return next.invoke(context); + } + } +} +*/ From a6ccd48c4906e354c0d9eaef043c048f396627a1 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 16:54:22 +0800 Subject: [PATCH 07/16] Add ApplicationContextServiceResolver and ServiceResolverConfiguration for service management --- spring/pom.xml | 46 +++++++++++++ .../ApplicationContextServiceResolver.java | 68 +++++++++++++++++++ .../ServiceResolverConfiguration.java | 13 ++++ 3 files changed, 127 insertions(+) create mode 100644 spring/pom.xml create mode 100644 spring/src/main/java/com/euonia/reflection/ApplicationContextServiceResolver.java create mode 100644 spring/src/main/java/com/euonia/reflection/ServiceResolverConfiguration.java diff --git a/spring/pom.xml b/spring/pom.xml new file mode 100644 index 0000000..89f8caa --- /dev/null +++ b/spring/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + com.euonia + parent + ${revision} + + + spring + euonia spring module + + + + com.euonia + core + ${revision} + + + + org.springframework + spring-context + 7.0.6 + + + + org.junit.jupiter + junit-jupiter + 5.12.2 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.3 + + + + + diff --git a/spring/src/main/java/com/euonia/reflection/ApplicationContextServiceResolver.java b/spring/src/main/java/com/euonia/reflection/ApplicationContextServiceResolver.java new file mode 100644 index 0000000..03fe3a6 --- /dev/null +++ b/spring/src/main/java/com/euonia/reflection/ApplicationContextServiceResolver.java @@ -0,0 +1,68 @@ +package com.euonia.reflection; + +import java.lang.reflect.Constructor; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.ApplicationContext; + +public class ApplicationContextServiceResolver implements ServiceResolver { + private final ApplicationContext applicationContext; + + public ApplicationContextServiceResolver(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public T getService(Class type) { + return applicationContext.getBeanProvider(type).getIfAvailable(); + } + + @Override + public T getRequiredService(Class type) { + return applicationContext.getBean(type); + } + + @Override + public T createInstance(Class type, Object... constructorArguments) { + AutowireCapableBeanFactory factory = applicationContext.getAutowireCapableBeanFactory(); + if (constructorArguments == null || constructorArguments.length == 0) { + return type.cast(factory.createBean(type)); + //return type.cast(factory.createBean(type, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false)); + } + + Constructor[] constructors = type.getDeclaredConstructors(); + for (Constructor constructor : constructors) { + Class[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length != constructorArguments.length) { + continue; + } + + if (!isAssignable(parameterTypes, constructorArguments)) { + continue; + } + + constructor.setAccessible(true); + @SuppressWarnings("unchecked") + T instance = (T) BeanUtils.instantiateClass(constructor, constructorArguments); + factory.autowireBean(instance); + return type.cast(factory.initializeBean(instance, type.getName())); + } + + throw new IllegalStateException("Could not find matching constructor for " + type.getName()); + } + + private static boolean isAssignable(Class[] parameterTypes, Object[] args) { + for (int i = 0; i < parameterTypes.length; i++) { + Object arg = args[i]; + if (arg == null) { + continue; + } + + if (!parameterTypes[i].isAssignableFrom(arg.getClass())) { + return false; + } + } + return true; + } +} diff --git a/spring/src/main/java/com/euonia/reflection/ServiceResolverConfiguration.java b/spring/src/main/java/com/euonia/reflection/ServiceResolverConfiguration.java new file mode 100644 index 0000000..4ea57dc --- /dev/null +++ b/spring/src/main/java/com/euonia/reflection/ServiceResolverConfiguration.java @@ -0,0 +1,13 @@ +package com.euonia.reflection; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ServiceResolverConfiguration { + @Bean + public ServiceResolver serviceResolver(ApplicationContext applicationContext) { + return new ApplicationContextServiceResolver(applicationContext); + } +} From 410787d7ea70fdbe36fb724b3b305bdab189f480 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 16:54:48 +0800 Subject: [PATCH 08/16] Add missing modules 'pipeline' and 'spring' to the parent POM --- pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pom.xml b/pom.xml index 0bc1bad..e7df6ed 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,8 @@ core osba domain + pipeline + spring euonia-parent From aeede669766890b38da81f1d1e5239c9ec326ed4 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 17:26:17 +0800 Subject: [PATCH 09/16] Refactor pipeline behavior annotations to use PipelineBehaviors and remove PipelineBehaviorType --- .../java/com/euonia/pipeline/PipelineBase.java | 14 +++++++++----- .../com/euonia/pipeline/PipelineBehaviorType.java | 14 -------------- .../com/euonia/pipeline/PipelineBehaviors.java | 2 +- .../pipeline/RequestResponsePipelineBase.java | 14 +++++++++----- .../com/euonia/pipeline/UsePipelineBehavior.java | 14 ++++++++++++++ .../pipeline/DefaultPipelineProviderTests.java | 2 +- 6 files changed, 34 insertions(+), 26 deletions(-) delete mode 100644 pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviorType.java create mode 100644 pipeline/src/main/java/com/euonia/pipeline/UsePipelineBehavior.java diff --git a/pipeline/src/main/java/com/euonia/pipeline/PipelineBase.java b/pipeline/src/main/java/com/euonia/pipeline/PipelineBase.java index bc51d78..e9f7f86 100644 --- a/pipeline/src/main/java/com/euonia/pipeline/PipelineBase.java +++ b/pipeline/src/main/java/com/euonia/pipeline/PipelineBase.java @@ -39,17 +39,21 @@ public Pipeline use(Class type, Object... args) { @Override public Pipeline useOf(Class contextType, boolean useAheadOfOthers) { - PipelineBehaviorType[] attributes = contextType.getAnnotationsByType(PipelineBehaviorType.class); + PipelineBehaviors annotation = contextType.getAnnotation(PipelineBehaviors.class); + if (annotation == null) { + return this; + } if (useAheadOfOthers) { - for (int index = 0; index < attributes.length; index++) { - Class behaviorType = attributes[index].behaviorType(); + Class[] behaviorTypes = annotation.value(); + for (int index = 0; index < behaviorTypes.length; index++) { + Class behaviorType = behaviorTypes[index]; use(next -> getNext(next, behaviorType), index); } return this; } - for (PipelineBehaviorType attribute : attributes) { - use(attribute.behaviorType()); + for (Class behaviorType : annotation.value()) { + use(behaviorType); } return this; } diff --git a/pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviorType.java b/pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviorType.java deleted file mode 100644 index f3a4dc9..0000000 --- a/pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviorType.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.euonia.pipeline; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -@Repeatable(PipelineBehaviors.class) -public @interface PipelineBehaviorType { - Class behaviorType(); -} diff --git a/pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviors.java b/pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviors.java index ad23442..79dc753 100644 --- a/pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviors.java +++ b/pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviors.java @@ -8,5 +8,5 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface PipelineBehaviors { - PipelineBehaviorType[] value(); + Class[] value(); } diff --git a/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineBase.java b/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineBase.java index 840c856..c0da33c 100644 --- a/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineBase.java +++ b/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineBase.java @@ -39,17 +39,21 @@ public RequestResponsePipeline use(Class type, Object... @Override public RequestResponsePipeline useOf(Class contextType, boolean useAheadOfOthers) { - PipelineBehaviorType[] attributes = contextType.getAnnotationsByType(PipelineBehaviorType.class); + PipelineBehaviors annotation = contextType.getAnnotation(PipelineBehaviors.class); + if (annotation == null) { + return this; + } if (useAheadOfOthers) { - for (int index = 0; index < attributes.length; index++) { - Class behaviorType = attributes[index].behaviorType(); + Class[] behaviorTypes = annotation.value(); + for (int index = 0; index < behaviorTypes.length; index++) { + Class behaviorType = behaviorTypes[index]; use(next -> getNext(next, behaviorType), index); } return this; } - for (PipelineBehaviorType attribute : attributes) { - use(attribute.behaviorType()); + for (Class behaviorType : annotation.value()) { + use(behaviorType); } return this; } diff --git a/pipeline/src/main/java/com/euonia/pipeline/UsePipelineBehavior.java b/pipeline/src/main/java/com/euonia/pipeline/UsePipelineBehavior.java new file mode 100644 index 0000000..80864eb --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/UsePipelineBehavior.java @@ -0,0 +1,14 @@ +// package com.euonia.pipeline; + +// import java.lang.annotation.ElementType; +// import java.lang.annotation.Repeatable; +// import java.lang.annotation.Retention; +// import java.lang.annotation.RetentionPolicy; +// import java.lang.annotation.Target; + +// @Retention(RetentionPolicy.RUNTIME) +// @Target(ElementType.TYPE) +// @Repeatable(PipelineBehaviors.class) +// public @interface UsePipelineBehavior { +// Class value(); +// } diff --git a/pipeline/src/test/java/com/euonia/pipeline/DefaultPipelineProviderTests.java b/pipeline/src/test/java/com/euonia/pipeline/DefaultPipelineProviderTests.java index edf1cae..7aad5b2 100644 --- a/pipeline/src/test/java/com/euonia/pipeline/DefaultPipelineProviderTests.java +++ b/pipeline/src/test/java/com/euonia/pipeline/DefaultPipelineProviderTests.java @@ -39,7 +39,7 @@ void should_resolve_method_parameters_from_service_resolver() { assertEquals("ctx", markerService.value); } - @PipelineBehaviorType(behaviorType = AnnotatedPipelineBehavior.class) + @PipelineBehaviors(AnnotatedPipelineBehavior.class) static class TraceContext { final List trace = new ArrayList<>(); } From 2830213465667077738477c76795240477b254a8 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 17:27:17 +0800 Subject: [PATCH 10/16] Refactor exception handling in DefaultPipelineProvider and remove unused UsePipelineBehavior class --- .../euonia/pipeline/DefaultPipelineProvider.java | 3 ++- .../com/euonia/pipeline/UsePipelineBehavior.java | 14 -------------- 2 files changed, 2 insertions(+), 15 deletions(-) delete mode 100644 pipeline/src/main/java/com/euonia/pipeline/UsePipelineBehavior.java diff --git a/pipeline/src/main/java/com/euonia/pipeline/DefaultPipelineProvider.java b/pipeline/src/main/java/com/euonia/pipeline/DefaultPipelineProvider.java index c1ecdbc..3e4ea86 100644 --- a/pipeline/src/main/java/com/euonia/pipeline/DefaultPipelineProvider.java +++ b/pipeline/src/main/java/com/euonia/pipeline/DefaultPipelineProvider.java @@ -1,5 +1,6 @@ package com.euonia.pipeline; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Comparator; @@ -52,7 +53,7 @@ private static CompletionStage invokeVoidLike(Method method, Object instan return stage.thenApply(ignored -> null); } return CompletableFuture.completedFuture(null); - } catch (Exception ex) { + } catch (IllegalAccessException | InvocationTargetException ex) { return CompletableFuture.failedFuture(ex); } } diff --git a/pipeline/src/main/java/com/euonia/pipeline/UsePipelineBehavior.java b/pipeline/src/main/java/com/euonia/pipeline/UsePipelineBehavior.java deleted file mode 100644 index 80864eb..0000000 --- a/pipeline/src/main/java/com/euonia/pipeline/UsePipelineBehavior.java +++ /dev/null @@ -1,14 +0,0 @@ -// package com.euonia.pipeline; - -// import java.lang.annotation.ElementType; -// import java.lang.annotation.Repeatable; -// import java.lang.annotation.Retention; -// import java.lang.annotation.RetentionPolicy; -// import java.lang.annotation.Target; - -// @Retention(RetentionPolicy.RUNTIME) -// @Target(ElementType.TYPE) -// @Repeatable(PipelineBehaviors.class) -// public @interface UsePipelineBehavior { -// Class value(); -// } From ababf45be714eab5ff570dbc0d3e4411b012b759 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 17:36:57 +0800 Subject: [PATCH 11/16] Add pipeline and spring modules to README, including detailed descriptions and usage examples --- README.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0c099d8..cea98bd 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,19 @@ graph TD direction TB Domain --> Core OSBA --> Core + Pipeline --> Core + Spring --> Core Sample --> Domain Sample --> OSBA + Sample --> Pipeline + Sample --> Spring end style Core fill:#4A90D9,color:#fff style Domain fill:#50B86C,color:#fff style OSBA fill:#E8833A,color:#fff + style Pipeline fill:#E74C3C,color:#fff + style Spring fill:#2ECC71,color:#fff style Sample fill:#9B59B6,color:#fff ``` @@ -51,6 +57,55 @@ graph TD | `EventAggregate` | Event metadata wrapper: id, eventId, typeName, originator, timestamp, sequence | | `@Audited` / `AuditRecord` / `AuditStore` | Change auditing support for domain entities | +### Pipeline (`euonia-pipeline`) +> Middleware pipeline framework inspired by ASP.NET Core pipeline pattern — chainable request/response processing with behaviors, delegates, and dependency injection integration. + +| Interface / Class | Description | +|-------------------|-------------| +| `Pipeline` | Pipeline builder: chain components via `use()`, build delegate, run async | +| `PipelineBase` | Abstract base with component registration, reverse-chain build, and `@PipelineBehaviors` annotation support | +| `PipelineDelegate` | `FunctionalInterface`: `CompletionStage invoke(Object context)` | +| `PipelineBehavior` | Behavior interface: `CompletionStage handleAsync(Object, PipelineDelegate)` | +| `PipelineFactory` / `DefaultPipelineFactory` | Factory for creating `Pipeline` and `RequestResponsePipeline` instances | +| `DefaultPipelineProvider` | Default implementation resolving behaviors via `ServiceResolver` (reflection or DI) | +| `RequestResponsePipeline` | Typed pipeline with request/response — supports `runAsync(TRequest)` | +| `RequestResponsePipelineBase` | Abstract base for typed pipelines | +| `RequestResponsePipelineBehavior` | Typed behavior: `handleAsync(TRequest, PipelineDelegate)` | +| `RequestResponsePipelineDelegate` | Typed delegate: `CompletionStage invoke(TRequest)` | +| `RequestPipelineDelegate` | Fire-and-forget typed delegate: `CompletionStage invoke(TRequest)` | +| `@PipelineBehaviors` | Annotation to auto-attach behaviors by context type | + +**Key features:** +- Fluent API: chain behaviors via `.use()` with lambda, class, or `@PipelineBehaviors` discovery +- Supports both void-pipeline (`Pipeline`) and request/response pipeline (`RequestResponsePipeline`) +- Delegate-based composition with reverse-chain construction (innermost executes first) +- `ServiceResolver` abstraction enables both standalone and Spring-integrated usage +- Async throughout via `CompletionStage` + +```java +// Create a pipeline +Pipeline pipeline = new DefaultPipelineProvider(resolver) + .use((ctx, next) -> next.invoke(ctx).thenRun(() -> System.out.println("Log: done"))) + .use(LoggingBehavior.class); + +// Run +pipeline.runAsync(new MyContext()).toCompletableFuture().join(); +``` + +### Spring (`euonia-spring`) +> Spring Framework integration module. Bridges `ServiceResolver` with Spring's `ApplicationContext` for seamless dependency injection in pipeline and other Euonia components. + +| Class | Description | +|-------|-------------| +| `ApplicationContextServiceResolver` | `ServiceResolver` implementation backed by Spring's `ApplicationContext` — supports `getBeanProvider`, `autowireBean`, and constructor-argument-based bean creation | +| `ServiceResolverConfiguration` | Spring `@Configuration` auto-wiring `ServiceResolver` as a bean | + +**Key features:** +- Enables Spring DI for pipeline behaviors and other Euonia components +- Auto-wires Spring-managed beans into pipeline delegates +- Fallback to reflection-based construction with autowiring support +- Minimal setup: just `@Import(ServiceResolverConfiguration.class)` or component-scan + ### OSBA (`euonia-osba`) > **Object-Oriented Scalable Business Architecture** — a rich business object framework with rule-based validation, property change tracking, state management, and reflection-driven factories. @@ -109,22 +164,55 @@ The `sample` module demonstrates **Euonia framework integration with Spring Boot | Category | Technology | |----------|-----------| | **Language** | Java 17+ (sample uses Java 25) | -| **Framework** | Spring Boot 4.0 (Spring MVC, Spring Data JPA) | +| **Framework** | Spring Boot 4.0 (Spring MVC, Spring Data JPA, Spring Framework 7.0) | | **Database** | MySQL, H2 (in-memory for testing) | | **API Docs** | SpringDoc OpenAPI 3.0 | | **Build** | Maven | | **ID Generation** | Snowflake, UUID, ULID | +| **Pipeline** | Custom middleware pipeline (chain-of-responsibility / middleware pattern) | +| **DI Integration** | Spring `ApplicationContext` via `ServiceResolver` abstraction | --- ## Quick Start +### Maven Dependencies + ```xml + + + com.euonia + core + 1.0.0 + + + + + com.euonia + pipeline + 1.0.0 + + + + + com.euonia + spring + 1.0.0 + + + com.euonia osba 1.0.0 + + + + com.euonia + domain + 1.0.0 + ``` ```java @@ -178,7 +266,7 @@ mvn spring-boot:run ## Donate -donate +donate --- From c1af067d5dd169ab0cc013ccaab19da3c90ee9d4 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 17:38:55 +0800 Subject: [PATCH 12/16] Update donate image URL in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cea98bd..99b34a2 100644 --- a/README.md +++ b/README.md @@ -266,7 +266,7 @@ mvn spring-boot:run ## Donate -donate +donate --- From 2b0fbf3f1da0f799cb006842a57161a5c861df9c Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 17:49:18 +0800 Subject: [PATCH 13/16] Update README files with detailed descriptions and examples for the Pipeline module --- pipeline/README.md | 406 ++++++++++++++++++++++++++++++++++++++++- pipeline/README.zh.md | 413 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 817 insertions(+), 2 deletions(-) create mode 100644 pipeline/README.zh.md diff --git a/pipeline/README.md b/pipeline/README.md index 27bc7cc..2123dc2 100644 --- a/pipeline/README.md +++ b/pipeline/README.md @@ -1,7 +1,268 @@ -# Configure Pipeline +# Pipeline Module -``` java +A lightweight, async middleware pipeline framework for Java, inspired by ASP.NET Core's pipeline pattern. Enables chainable request/response processing with behaviors, delegates, and pluggable dependency injection. + +--- + +## Architecture + +``` +Request / Context + │ + ▼ +┌──────────────────────┐ +│ Pipeline.use(…) │ ← Behavior 1 (logging, auth, validation…) +├──────────────────────┤ +│ Pipeline.use(…) │ ← Behavior 2 (transformation, enrichment…) +├──────────────────────┤ +│ Pipeline.use(…) │ ← Behavior N +├──────────────────────┤ +│ Accumulate / Handler│ ← Terminal handler (user logic) +└──────────────────────┘ + │ + ▼ + Response / Void +``` + +Each behavior is a **middleware** that receives the context and a `next` delegate. Behaviors can: +- Execute code **before** the next component +- Execute code **after** the next component (via the returned `CompletionStage`) +- Short-circuit the pipeline by **not calling** `next.invoke()` +- **Modify** the context before passing it downstream + +--- + +## Core Concepts + +### Pipeline (Fire-and-Forget) + +| Interface / Class | Description | +|-------------------|-------------| +| `Pipeline` | Builder interface — chain behaviors via `.use()`, then `.build()` or `.runAsync()` | +| `PipelineBase` | Abstract implementation with component list, reverse-chain build, and `@PipelineBehaviors` support | +| `PipelineDelegate` | `FunctionalInterface` — `CompletionStage invoke(Object context)` | +| `PipelineBehavior` | Behavior contract — `CompletionStage handleAsync(Object, PipelineDelegate)` | + +### RequestResponsePipeline (Typed Request/Response) + +| Interface / Class | Description | +|-------------------|-------------| +| `RequestResponsePipeline` | Builder for typed request/response pipelines | +| `RequestResponsePipelineBase` | Abstract implementation | +| `RequestResponsePipelineDelegate` | `CompletionStage invoke(TRequest)` | +| `RequestResponsePipelineBehavior` | `CompletionStage handleAsync(TRequest, PipelineDelegate)` | +| `RequestPipelineDelegate` | Fire-and-forget variant: `CompletionStage invoke(TRequest)` | + +### Infrastructure + +| Interface / Class | Description | +|-------------------|-------------| +| `PipelineFactory` | Creates `Pipeline` or `RequestResponsePipeline` instances | +| `DefaultPipelineFactory` | Default factory backed by `ServiceResolver` | +| `DefaultPipelineProvider` | Default `Pipeline` implementation | +| `DefaultRequestResponsePipelineProvider` | Default typed pipeline implementation | +| `@PipelineBehaviors` | Annotation to auto-discover behaviors from context type | +| `ServiceResolver` | Abstraction for DI — standalone (`SimpleServiceResolver`) or Spring integration | + +--- + +## Getting Started + +### Step 1: Add Dependency + +```xml + + com.euonia + pipeline + 1.0.0 + +``` + +### Step 2: Basic Pipeline + +```java +import com.euonia.pipeline.*; +import com.euonia.reflection.SimpleServiceResolver; + +// Create resolver and pipeline +var resolver = new SimpleServiceResolver(); +Pipeline pipeline = new DefaultPipelineProvider(resolver) + .use((ctx, next) -> { + System.out.println("Before: " + ctx); + return next.invoke(ctx).thenRun(() -> System.out.println("After: " + ctx)); + }); + +// Run +pipeline.runAsync("Hello, Pipeline!") + .toCompletableFuture() + .join(); +``` + +### Step 3: Custom Behavior Class + +```java +public class LoggingBehavior implements PipelineBehavior { + @Override + public CompletionStage handleAsync(Object context, PipelineDelegate next) { + long start = System.nanoTime(); + return next.invoke(context).thenRun(() -> { + long elapsed = (System.nanoTime() - start) / 1_000_000; + System.out.println("[" + context.getClass().getSimpleName() + "] completed in " + elapsed + "ms"); + }); + } +} + +// Usage +Pipeline pipeline = new DefaultPipelineProvider(resolver) + .use(LoggingBehavior.class) + .use((ctx, next) -> { + // business logic + return next.invoke(ctx); + }); +``` + +### Step 4: Request/Response Pipeline + +```java +DefaultRequestResponsePipelineProvider pipeline = + new DefaultRequestResponsePipelineProvider<>(resolver); + +pipeline.use(PlusOneBehavior.class); + +int result = pipeline.runAsync(2, request -> CompletableFuture.completedFuture(request * 2)) + .toCompletableFuture() + .join(); + +// result == 5 (2 * 2 + 1) +``` + +**PlusOneBehavior:** +```java +public class PlusOneBehavior implements RequestResponsePipelineBehavior { + @Override + public CompletionStage handleAsync(Integer context, + RequestResponsePipelineDelegate next) { + return next.invoke(context).thenApply(value -> value + 1); + } +} +``` + +--- + +## Usage Examples + +### Lambda Behaviors + +```java +// Inline lambda (fire-and-forget) +pipeline.use((ctx, next) -> { + System.out.println("Processing: " + ctx); + return next.invoke(ctx); +}); + +// Inline lambda (request/response) +requestResponsePipeline.use((req, next) -> + next.invoke(req).thenApply(resp -> "Wrapped: " + resp) +); +``` + +### Dependency Injection via ServiceResolver + +Behaviors can declare extra parameters beyond the context — they are resolved automatically from the `ServiceResolver`. + +```java +// Define a service +public class SuffixService { + private final String suffix; + public SuffixService(String suffix) { this.suffix = suffix; } + public String apply(String value) { return value + suffix; } +} + +// Register it +resolver.register(SuffixService.class, new SuffixService("-ok")); + +// Pipeline behavior with auto-resolved dependency +public class ReflectionBehavior { + private final RequestResponsePipelineDelegate next; + + public ReflectionBehavior(RequestResponsePipelineDelegate next) { + this.next = next; + } + + public CompletionStage handleAsync(String context, SuffixService suffixService) { + return next.invoke(context).thenApply(value -> suffixService.apply(value)); + } +} + +// Usage +pipeline.use(ReflectionBehavior.class); +String result = pipeline.runAsync("input", CompletableFuture::completedFuture) + .toCompletableFuture() + .join(); +// result == "input-ok" +``` + +### `@PipelineBehaviors` — Auto-Discovery + +Annotate your context class to automatically attach relevant behaviors: + +```java +@PipelineBehaviors({ValidationBehavior.class, AuditBehavior.class}) +public class CreateOrderCommand { + // ... +} + +// When you call runAsync, the annotation is discovered automatically: +pipeline.runAsync(new CreateOrderCommand()) + .toCompletableFuture() + .join(); +// ValidationBehavior and AuditBehavior execute before any explicitly registered behaviors +``` + +### Fluent Builder with Composite Pipeline + +```java +Pipeline pipeline = resolver.create() // via PipelineFactory + .use(AuthenticationBehavior.class) + .use(AuthorizationBehavior.class) + .use(ValidationBehavior.class, 0) // insert at specific index + .use((ctx, next) -> next.invoke(ctx)) + .build(); // freezes the pipeline, clears component list + +pipeline.invoke(context).toCompletableFuture().join(); +``` + +### Resolver Dependency Parameters in `handle` / `handleAsync` + +Behaviors written as plain classes (not implementing `PipelineBehavior`) are resolved via reflection. The first parameter is the **context**, and all subsequent parameters are **auto-injected** from the `ServiceResolver`: + +```java +// Plain class — method name must be "handle" or "handleAsync" +// Return type must be CompletionStage +public class MyBehavior { + private final PipelineDelegate next; + + public MyBehavior(PipelineDelegate next) { + this.next = next; + } + + // context + auto-injected services + public CompletionStage handleAsync(MyContext ctx, LoggerService logger, MetricsService metrics) { + logger.info("Processing " + ctx); + metrics.increment(); + return next.invoke(ctx); + } +} +``` + +--- + +## Spring Boot Integration + +### Configuration + +```java @Configuration public class PipelineConfiguration { @Bean @@ -10,3 +271,144 @@ public class PipelineConfiguration { } } ``` + +### Using Spring-Managed Beans in Behaviors + +Behaviors can inject any Spring bean through constructor parameters. The `ApplicationContextServiceResolver` (from `euonia-spring` module) handles auto-wiring automatically. + +```java +@Component +public class SpringLoggingBehavior { + private final PipelineDelegate next; + private final LoggerService logger; // Spring bean + + public SpringLoggingBehavior(PipelineDelegate next, LoggerService logger) { + this.next = next; + this.logger = logger; + } + + public CompletionStage handleAsync(Object ctx) { + logger.info("Pipeline processing: " + ctx); + return next.invoke(ctx); + } +} +``` + +```java +@Autowired +private PipelineFactory pipelineFactory; + +public void execute() { + Pipeline pipeline = pipelineFactory.create() + .use(SpringLoggingBehavior.class) + .use(TransactionalBehavior.class); + + pipeline.runAsync(new MyCommand()).toCompletableFuture().join(); +} +``` + +--- + +## API Reference + +### `Pipeline` + +```java +public interface Pipeline { + Pipeline use(Function component); + Pipeline use(Function component, int index); + Pipeline use(BiFunction> handler); + Pipeline use(Class type, Object... args); + Pipeline useOf(Class contextType, boolean useAheadOfOthers); + PipelineDelegate build(); + CompletionStage runAsync(Object context); + CompletionStage runAsync(Object context, Function> accumulate); +} +``` + +| Method | Description | +|--------|-------------| +| `use(component)` | Appends a pipeline component | +| `use(component, index)` | Inserts a component at the given position | +| `use(handler)` | Appends a lambda handler `(ctx, next) → CompletionStage` | +| `use(type, args)` | Appends a component resolved from the given class with constructor arguments | +| `useOf(contextType, ahead)` | Auto-discovers `@PipelineBehaviors` annotation on the context type | +| `build()` | Freezes the pipeline and returns the outermost delegate | +| `runAsync(context)` | Shorthand: calls `useOf` then `build().invoke(context)` | +| `runAsync(context, accumulate)` | Shorthand with terminal handler | + +### `PipelineBehavior` + +```java +@FunctionalInterface +public interface PipelineBehavior { + CompletionStage handleAsync(Object context, PipelineDelegate next); +} +``` + +### `RequestResponsePipeline` + +Same fluent API as `Pipeline`, but typed with `TRequest` / `TResponse`: + +```java +CompletionStage runAsync(TRequest context); +CompletionStage runAsync(TRequest context, Function> accumulate); +``` + +--- + +## Design & Implementation Details + +### Reverse-Chain Construction + +When `.build()` is called, components are assembled **inside-out** — the last registered component wraps the previous ones. This means: + +```java +pipeline.use(A).use(B).use(C); +// Execution order: A → B → C +// Construction: C wraps B wraps A +``` + +### Behavior Resolution Priority + +1. **`PipelineBehavior` interface** — if the class implements `PipelineBehavior`, it is resolved via `ServiceResolver.getServiceOrCreate()` and invoked through the interface contract. +2. **Reflection-based** — otherwise, the framework searches for `handle` or `handleAsync` methods (returning `CompletionStage`). Constructor arguments are populated by prepending the `next` delegate. + +### Annotation-Driven Auto-Discovery + +The `@PipelineBehaviors` annotation enables **declarative pipeline configuration**: + +```java +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface PipelineBehaviors { + Class[] value(); +} +``` + +When `runAsync(context)` is called (or `useOf(contextType, true)` explicitly), the annotation on the context's class is scanned. Behaviors listed in the annotation are registered **ahead of** all manually registered components. + +--- + +## Testing + +The pipeline module is designed for testability: + +```java +// Unit test with SimpleServiceResolver +var resolver = new SimpleServiceResolver(); +var pipeline = new DefaultPipelineProvider(resolver); + +var results = new ArrayList(); +pipeline.use((ctx, next) -> { + results.add("before"); + return next.invoke(ctx).thenRun(() -> results.add("after")); +}); +pipeline.use((ctx, next) -> { + results.add("handle"); + return next.invoke(ctx); +}); + +pipeline.runAsync("test").toCompletableFuture().join(); +assertEquals(List.of("before", "handle", "after"), results); +``` diff --git a/pipeline/README.zh.md b/pipeline/README.zh.md new file mode 100644 index 0000000..e920627 --- /dev/null +++ b/pipeline/README.zh.md @@ -0,0 +1,413 @@ +# Pipeline 模块 + +一个轻量级的异步中间件管道框架,受 ASP.NET Core 管道模式启发。支持可链式组合的请求/响应处理,包含行为(Behaviors)、委托(Delegates)和可插拔的依赖注入。 + +--- + +## 架构 + +``` +请求 / 上下文 + │ + ▼ +┌──────────────────────┐ +│ Pipeline.use(…) │ ← 行为 1(日志、认证、验证…) +├──────────────────────┤ +│ Pipeline.use(…) │ ← 行为 2(转换、增强…) +├──────────────────────┤ +│ Pipeline.use(…) │ ← 行为 N +├──────────────────────┤ +│ Accumulate / Handler│ ← 终端处理器(用户逻辑) +└──────────────────────┘ + │ + ▼ + 响应 / Void +``` + +每个行为是一个**中间件**,接收上下文和一个 `next` 委托。行为可以: +- 在下一个组件**之前**执行代码 +- 在下一个组件**之后**执行代码(通过返回的 `CompletionStage`) +- **不调用** `next.invoke()` 来**短路**管道 +- 在传入下游之前**修改**上下文 + +--- + +## 核心概念 + +### Pipeline(即发即忘) + +| 接口 / 类 | 描述 | +|-----------|------| +| `Pipeline` | 构建器接口 — 通过 `.use()` 链式组合行为,然后调用 `.build()` 或 `.runAsync()` | +| `PipelineBase` | 抽象实现,包含组件列表、反向链构建和 `@PipelineBehaviors` 支持 | +| `PipelineDelegate` | `FunctionalInterface` — `CompletionStage invoke(Object context)` | +| `PipelineBehavior` | 行为契约 — `CompletionStage handleAsync(Object, PipelineDelegate)` | + +### RequestResponsePipeline(类型化的请求/响应) + +| 接口 / 类 | 描述 | +|-----------|------| +| `RequestResponsePipeline` | 类型化请求/响应管道的构建器 | +| `RequestResponsePipelineBase` | 抽象实现 | +| `RequestResponsePipelineDelegate` | `CompletionStage invoke(TRequest)` | +| `RequestResponsePipelineBehavior` | `CompletionStage handleAsync(TRequest, PipelineDelegate)` | +| `RequestPipelineDelegate` | 即发即忘变体:`CompletionStage invoke(TRequest)` | + +### 基础设施 + +| 接口 / 类 | 描述 | +|-----------|------| +| `PipelineFactory` | 创建 `Pipeline` 或 `RequestResponsePipeline` 实例 | +| `DefaultPipelineFactory` | 基于 `ServiceResolver` 的默认工厂 | +| `DefaultPipelineProvider` | 默认的 `Pipeline` 实现 | +| `DefaultRequestResponsePipelineProvider` | 默认的类型化管道实现 | +| `@PipelineBehaviors` | 从上下文类型自动发现行为的注解 | +| `ServiceResolver` | DI 抽象 — 独立使用(`SimpleServiceResolver`)或 Spring 集成 | + +--- + +## 快速入门 + +### 第一步:添加依赖 + +```xml + + com.euonia + pipeline + 1.0.0 + +``` + +### 第二步:基础管道 + +```java +import com.euonia.pipeline.*; +import com.euonia.reflection.SimpleServiceResolver; + +// 创建解析器和管道 +var resolver = new SimpleServiceResolver(); +Pipeline pipeline = new DefaultPipelineProvider(resolver) + .use((ctx, next) -> { + System.out.println("Before: " + ctx); + return next.invoke(ctx).thenRun(() -> System.out.println("After: " + ctx)); + }); + +// 运行 +pipeline.runAsync("Hello, Pipeline!") + .toCompletableFuture() + .join(); +``` + +### 第三步:自定义行为类 + +```java +public class LoggingBehavior implements PipelineBehavior { + @Override + public CompletionStage handleAsync(Object context, PipelineDelegate next) { + long start = System.nanoTime(); + return next.invoke(context).thenRun(() -> { + long elapsed = (System.nanoTime() - start) / 1_000_000; + System.out.println("[" + context.getClass().getSimpleName() + "] 耗时 " + elapsed + "ms"); + }); + } +} + +// 使用 +Pipeline pipeline = new DefaultPipelineProvider(resolver) + .use(LoggingBehavior.class) + .use((ctx, next) -> { + // 业务逻辑 + return next.invoke(ctx); + }); +``` + +### 第四步:请求/响应管道 + +```java +DefaultRequestResponsePipelineProvider pipeline = + new DefaultRequestResponsePipelineProvider<>(resolver); + +pipeline.use(PlusOneBehavior.class); + +int result = pipeline.runAsync(2, request -> CompletableFuture.completedFuture(request * 2)) + .toCompletableFuture() + .join(); + +// result == 5 (2 * 2 + 1) +``` + +**PlusOneBehavior:** +```java +public class PlusOneBehavior implements RequestResponsePipelineBehavior { + @Override + public CompletionStage handleAsync(Integer context, + RequestResponsePipelineDelegate next) { + return next.invoke(context).thenApply(value -> value + 1); + } +} +``` + +--- + +## 使用示例 + +### Lambda 行为 + +```java +// 内联 lambda(即发即忘) +pipeline.use((ctx, next) -> { + System.out.println("Processing: " + ctx); + return next.invoke(ctx); +}); + +// 内联 lambda(请求/响应) +requestResponsePipeline.use((req, next) -> + next.invoke(req).thenApply(resp -> "Wrapped: " + resp) +); +``` + +### 通过 ServiceResolver 进行依赖注入 + +行为可以声明除上下文以外的额外参数 — 它们会从 `ServiceResolver` 自动解析。 + +```java +// 定义服务 +public class SuffixService { + private final String suffix; + public SuffixService(String suffix) { this.suffix = suffix; } + public String apply(String value) { return value + suffix; } +} + +// 注册 +resolver.register(SuffixService.class, new SuffixService("-ok")); + +// 具有自动解析依赖的管道行为 +public class ReflectionBehavior { + private final RequestResponsePipelineDelegate next; + + public ReflectionBehavior(RequestResponsePipelineDelegate next) { + this.next = next; + } + + public CompletionStage handleAsync(String context, SuffixService suffixService) { + return next.invoke(context).thenApply(value -> suffixService.apply(value)); + } +} + +// 使用 +pipeline.use(ReflectionBehavior.class); +String result = pipeline.runAsync("input", CompletableFuture::completedFuture) + .toCompletableFuture() + .join(); +// result == "input-ok" +``` + +### `@PipelineBehaviors` — 自动发现 + +在上下文类上添加注解,自动附加相关的行为: + +```java +@PipelineBehaviors({ValidationBehavior.class, AuditBehavior.class}) +public class CreateOrderCommand { + // ... +} + +// 调用 runAsync 时,注解会被自动发现: +pipeline.runAsync(new CreateOrderCommand()) + .toCompletableFuture() + .join(); +// ValidationBehavior 和 AuditBehavior 会在所有手动注册的行为之前执行 +``` + +### Fluent Builder 与复合管道 + +```java +Pipeline pipeline = resolver.create() // 通过 PipelineFactory + .use(AuthenticationBehavior.class) + .use(AuthorizationBehavior.class) + .use(ValidationBehavior.class, 0) // 插入到指定索引位置 + .use((ctx, next) -> next.invoke(ctx)) + .build(); // 冻结管道,清空组件列表 + +pipeline.invoke(context).toCompletableFuture().join(); +``` + +### 在 `handle` / `handleAsync` 中解析依赖参数 + +使用普通类(不实现 `PipelineBehavior`)编写的行为通过反射解析。第一个参数是**上下文**,后续参数从 `ServiceResolver` **自动注入**: + +```java +// 普通类 — 方法名必须为 "handle" 或 "handleAsync" +// 返回类型必须为 CompletionStage +public class MyBehavior { + private final PipelineDelegate next; + + public MyBehavior(PipelineDelegate next) { + this.next = next; + } + + // 上下文 + 自动注入的服务 + public CompletionStage handleAsync(MyContext ctx, LoggerService logger, MetricsService metrics) { + logger.info("Processing " + ctx); + metrics.increment(); + return next.invoke(ctx); + } +} +``` + +--- + +## Spring Boot 集成 + +### 配置 + +```java +@Configuration +public class PipelineConfiguration { + @Bean + public PipelineFactory pipelineFactory(ServiceResolver resolver) { + return new DefaultPipelineFactory(resolver); + } +} +``` + +### 在行为中使用 Spring 管理的 Bean + +行为可以通过构造函数参数注入任何 Spring Bean。`ApplicationContextServiceResolver`(来自 `euonia-spring` 模块)会自动处理自动装配。 + +```java +@Component +public class SpringLoggingBehavior { + private final PipelineDelegate next; + private final LoggerService logger; // Spring Bean + + public SpringLoggingBehavior(PipelineDelegate next, LoggerService logger) { + this.next = next; + this.logger = logger; + } + + public CompletionStage handleAsync(Object ctx) { + logger.info("管道处理: " + ctx); + return next.invoke(ctx); + } +} +``` + +```java +@Autowired +private PipelineFactory pipelineFactory; + +public void execute() { + Pipeline pipeline = pipelineFactory.create() + .use(SpringLoggingBehavior.class) + .use(TransactionalBehavior.class); + + pipeline.runAsync(new MyCommand()).toCompletableFuture().join(); +} +``` + +--- + +## API 参考 + +### `Pipeline` + +```java +public interface Pipeline { + Pipeline use(Function component); + Pipeline use(Function component, int index); + Pipeline use(BiFunction> handler); + Pipeline use(Class type, Object... args); + Pipeline useOf(Class contextType, boolean useAheadOfOthers); + PipelineDelegate build(); + CompletionStage runAsync(Object context); + CompletionStage runAsync(Object context, Function> accumulate); +} +``` + +| 方法 | 描述 | +|------|------| +| `use(component)` | 追加一个管道组件 | +| `use(component, index)` | 在指定位置插入一个组件 | +| `use(handler)` | 追加一个 lambda 处理器 `(ctx, next) → CompletionStage` | +| `use(type, args)` | 从指定类解析并追加一个组件,附带构造函数参数 | +| `useOf(contextType, ahead)` | 自动发现上下文类型上的 `@PipelineBehaviors` 注解 | +| `build()` | 冻结管道并返回最外层的委托 | +| `runAsync(context)` | 快捷方式:调用 `useOf` 后执行 `build().invoke(context)` | +| `runAsync(context, accumulate)` | 带有终端处理器的快捷方式 | + +### `PipelineBehavior` + +```java +@FunctionalInterface +public interface PipelineBehavior { + CompletionStage handleAsync(Object context, PipelineDelegate next); +} +``` + +### `RequestResponsePipeline` + +与 `Pipeline` 相同的 Fluent API,但带有 `TRequest` / `TResponse` 类型参数: + +```java +CompletionStage runAsync(TRequest context); +CompletionStage runAsync(TRequest context, Function> accumulate); +``` + +--- + +## 设计与实现细节 + +### 反向链式构建 + +调用 `.build()` 时,组件按**从内到外**的方式组装 — 最后注册的组件包裹前面的组件。这意味着: + +```java +pipeline.use(A).use(B).use(C); +// 执行顺序: A → B → C +// 构造顺序: C 包裹 B, B 包裹 A +``` + +### 行为解析优先级 + +1. **`PipelineBehavior` 接口** — 如果类实现了 `PipelineBehavior`,则通过 `ServiceResolver.getServiceOrCreate()` 解析,并通过接口契约调用。 +2. **基于反射** — 否则,框架搜索 `handle` 或 `handleAsync` 方法(返回 `CompletionStage`)。构造函数参数通过将 `next` 委托前置填充。 + +### 注解驱动的自动发现 + +`@PipelineBehaviors` 注解实现了**声明式管道配置**: + +```java +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface PipelineBehaviors { + Class[] value(); +} +``` + +当调用 `runAsync(context)`(或显式调用 `useOf(contextType, true)`)时,会扫描上下文类上的注解。注解中列出的行为会被注册到**所有手动注册组件之前**。 + +--- + +## 测试 + +管道模块设计为易于测试: + +```java +// 使用 SimpleServiceResolver 进行单元测试 +var resolver = new SimpleServiceResolver(); +var pipeline = new DefaultPipelineProvider(resolver); + +var results = new ArrayList(); +pipeline.use((ctx, next) -> { + results.add("before"); + return next.invoke(ctx).thenRun(() -> results.add("after")); +}); +pipeline.use((ctx, next) -> { + results.add("handle"); + return next.invoke(ctx); +}); + +pipeline.runAsync("test").toCompletableFuture().join(); +assertEquals(List.of("before", "handle", "after"), results); +``` From 8686895bb535d9f41f662cdf06c5a492c2baac66 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 18:15:33 +0800 Subject: [PATCH 14/16] Implement DelegateServiceResolver and update ServiceResolver interface with new methods --- .../reflection/DelegateServiceResolver.java | 90 +++++++++++++++ .../euonia/reflection/ServiceResolver.java | 43 +++++++ .../reflection/SimpleServiceResolver.java | 19 ++- .../com/euonia/reflection/TypeHelper.java | 109 ++++++++++++------ .../DelegateServiceResolverTest.java | 59 ++++++++++ 5 files changed, 279 insertions(+), 41 deletions(-) create mode 100644 core/src/main/java/com/euonia/reflection/DelegateServiceResolver.java create mode 100644 core/src/test/java/com/euonia/reflection/DelegateServiceResolverTest.java diff --git a/core/src/main/java/com/euonia/reflection/DelegateServiceResolver.java b/core/src/main/java/com/euonia/reflection/DelegateServiceResolver.java new file mode 100644 index 0000000..52fa3ab --- /dev/null +++ b/core/src/main/java/com/euonia/reflection/DelegateServiceResolver.java @@ -0,0 +1,90 @@ +package com.euonia.reflection; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.function.Function; + +/** + * DelegateServiceResolver is an implementation of the ServiceResolver interface + * that delegates service resolution to a provided Function. + * It allows for flexible service resolution by using a custom Function to + * retrieve services based on their class type. + * The createInstance method uses reflection to find a matching constructor and + * create an instance of the specified type with the provided constructor + * arguments. + */ +public class DelegateServiceResolver implements ServiceResolver { + + private final Function, ?> beanFactory; + + /** + * Creates a new instance of DelegateServiceResolver with the given bean factory + * function. + * + * @param beanFactory the function used to resolve services based on their class + * type + */ + public DelegateServiceResolver(Function, ?> beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public T getService(Class type) { + return (T) beanFactory.apply(type); + } + + @Override + public T getRequiredService(Class type) { + T instance = (T) beanFactory.apply(type); + if (instance == null) { + throw new IllegalStateException("Required service not found: " + type.getName()); + } + return instance; + } + + @Override + public T createInstance(Class type, Object... constructorArguments) { + Constructor[] constructors = type.getDeclaredConstructors(); + for (Constructor constructor : constructors) { + Class[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length != constructorArguments.length) { + continue; + } + + if (!isAssignable(parameterTypes, constructorArguments)) { + continue; + } + + try { + constructor.setAccessible(true); + T created = (T) constructor.newInstance(constructorArguments); + return created; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException("Could not construct type: " + type.getName(), e); + } + } + + try { + Constructor constructor = type.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException + | NoSuchMethodException e) { + throw new IllegalStateException("Could not construct type: " + type.getName(), e); + } + } + + private static boolean isAssignable(Class[] parameterTypes, Object[] args) { + for (int i = 0; i < parameterTypes.length; i++) { + Object arg = args[i]; + if (arg == null) { + continue; + } + Class parameterType = TypeHelper.boxIfPrimitive(parameterTypes[i]); + if (!parameterType.isAssignableFrom(arg.getClass())) { + return false; + } + } + return true; + } +} diff --git a/core/src/main/java/com/euonia/reflection/ServiceResolver.java b/core/src/main/java/com/euonia/reflection/ServiceResolver.java index d0a01a2..beb6f37 100644 --- a/core/src/main/java/com/euonia/reflection/ServiceResolver.java +++ b/core/src/main/java/com/euonia/reflection/ServiceResolver.java @@ -1,12 +1,55 @@ package com.euonia.reflection; +/** + * ServiceResolver is an interface that defines methods for resolving and + * creating service instances. + * Implementations of this interface can provide custom logic for retrieving and + * instantiating services. + */ public interface ServiceResolver { + /** + * Retrieves a service instance of the specified type. If the service is not + * found, it returns null. + * + * @param the type of the service + * @param type the class of the service + * @return the service instance or null if not found + */ T getService(Class type); + /** + * Retrieves a required service instance of the specified type. If the service + * is not found, it throws an exception. + * + * @param the type of the service + * @param type the class of the service + * @return the service instance + * @throws IllegalStateException if the service is not found + */ T getRequiredService(Class type); + /** + * Creates a new instance of the specified type using the provided constructor + * arguments. + * + * @param the type of the service + * @param type the class of the service + * @param constructorArguments the arguments to pass to the constructor + * @return the created instance + * @throws IllegalStateException if the instance cannot be created + */ T createInstance(Class type, Object... constructorArguments); + /** + * Retrieves a service instance of the specified type, or creates a new instance + * if the service is not found. + * + * @param the type of the service + * @param type the class of the service + * @param constructorArguments the arguments to pass to the constructor if a new + * instance is created + * @return the service instance or a newly created instance + */ default T getServiceOrCreate(Class type, Object... constructorArguments) { T service = getService(type); return service != null ? service : createInstance(type, constructorArguments); diff --git a/core/src/main/java/com/euonia/reflection/SimpleServiceResolver.java b/core/src/main/java/com/euonia/reflection/SimpleServiceResolver.java index d6cd912..ef82509 100644 --- a/core/src/main/java/com/euonia/reflection/SimpleServiceResolver.java +++ b/core/src/main/java/com/euonia/reflection/SimpleServiceResolver.java @@ -5,6 +5,16 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +/** + * SimpleServiceResolver is a basic implementation of the ServiceResolver + * interface + * that uses a ConcurrentHashMap to store and retrieve service instances. + * It allows for registering services and creating new instances using + * reflection. + * The createInstance method looks for a matching constructor based on the + * provided + * arguments and creates an instance of the specified type. + */ public class SimpleServiceResolver implements ServiceResolver { private final Map, Object> services = new ConcurrentHashMap<>(); @@ -13,7 +23,6 @@ public void register(Class type, T instance) { } @Override - @SuppressWarnings("unchecked") public T getService(Class type) { Object service = services.get(type); if (service == null) { @@ -26,7 +35,7 @@ public T getService(Class type) { public T getRequiredService(Class type) { T service = getService(type); if (service == null) { - throw new IllegalStateException("Can not resolve service: " + type.getName()); + throw new IllegalStateException("Cannot resolve service: " + type.getName()); } return service; } @@ -58,7 +67,8 @@ public T createInstance(Class type, Object... constructorArguments) { Constructor constructor = type.getDeclaredConstructor(); constructor.setAccessible(true); return constructor.newInstance(); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + } catch (InstantiationException | IllegalAccessException | InvocationTargetException + | NoSuchMethodException e) { throw new IllegalStateException("Could not construct type: " + type.getName(), e); } } @@ -69,7 +79,8 @@ private static boolean isAssignable(Class[] parameterTypes, Object[] args) { if (arg == null) { continue; } - if (!parameterTypes[i].isAssignableFrom(arg.getClass())) { + Class parameterType = TypeHelper.boxIfPrimitive(parameterTypes[i]); + if (!parameterType.isAssignableFrom(arg.getClass())) { return false; } } diff --git a/core/src/main/java/com/euonia/reflection/TypeHelper.java b/core/src/main/java/com/euonia/reflection/TypeHelper.java index 866f8f1..d8a5204 100644 --- a/core/src/main/java/com/euonia/reflection/TypeHelper.java +++ b/core/src/main/java/com/euonia/reflection/TypeHelper.java @@ -2,12 +2,31 @@ import java.lang.reflect.Array; import java.math.BigDecimal; -import java.time.*; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; import java.time.temporal.Temporal; -import java.util.*; - -@SuppressWarnings("unused") +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; + +/** + * TypeHelper is a utility class that provides methods for coercing values to a + * desired type, handling primitive types, enums, date/time conversions, + * collections, and more. + * It is designed to simplify type conversions and ensure type safety in various + * scenarios. + */ public final class TypeHelper { private TypeHelper() { } @@ -45,7 +64,7 @@ public static Object coerceValue(Class desiredType, Class valueType, Objec // Collections/Map/JSON if (Collection.class.isAssignableFrom(boxedDesired) || boxedDesired.isArray() - || Map.class.isAssignableFrom(boxedDesired)) { + || Map.class.isAssignableFrom(boxedDesired)) { return convertToCollectionOrMap(boxedDesired, value); } @@ -82,7 +101,7 @@ public static Object coerceValue(Class desiredType, Class valueType, Objec return conv; throw new IllegalArgumentException(String.format("Cannot convert value of type %s to %s (value=%s)", - value.getClass().getName(), desiredType.getName(), value)); + value.getClass().getName(), desiredType.getName(), value)); } @SuppressWarnings("unchecked") @@ -90,34 +109,50 @@ public static T coerceValue(Class desiredType, Object value) { return (T) coerceValue(desiredType, (value == null ? null : value.getClass()), value); } - private static Class boxIfPrimitive(Class c) { - if (!c.isPrimitive()) - return c; - if (c == int.class) + /** + * If the given class is a primitive type, returns its boxed type. Otherwise, + * returns the class itself. + * + * @param type the class to check + * @return the boxed type if the class is primitive, otherwise the class itself + */ + public static Class boxIfPrimitive(Class type) { + if (!type.isPrimitive()) { + return type; + } + if (type == int.class) { return Integer.class; - if (c == long.class) + } + if (type == long.class) { return Long.class; - if (c == short.class) + } + if (type == short.class) { return Short.class; - if (c == byte.class) + } + if (type == byte.class) { return Byte.class; - if (c == float.class) + } + if (type == float.class) { return Float.class; - if (c == double.class) + } + if (type == double.class) { return Double.class; - if (c == boolean.class) + } + if (type == boolean.class) { return Boolean.class; - if (c == char.class) + } + if (type == char.class) { return Character.class; - return c; + } + return type; } - private static boolean isPrimitiveNumber(Class c) { - return c == int.class || c == long.class || c == short.class || c == byte.class - || c == float.class || c == double.class; + public static boolean isPrimitiveNumber(Class type) { + return type == int.class || type == long.class || type == short.class || type == byte.class + || type == float.class || type == double.class; } - private static Object defaultPrimitiveValue(Class primitiveType) { + public static Object defaultPrimitiveValue(Class primitiveType) { if (primitiveType == boolean.class) return false; if (primitiveType == char.class) @@ -137,7 +172,7 @@ private static Object defaultPrimitiveValue(Class primitiveType) { return null; } - @SuppressWarnings({"rawtypes", "unchecked", "IfCanBeSwitch"}) + @SuppressWarnings({ "rawtypes", "unchecked", "IfCanBeSwitch" }) private static Object convertToEnum(Class enumType, Object value) { if (value == null) { @@ -265,9 +300,9 @@ private static Object convertToCharacter(Object value) { // available) private static boolean isDateTimeTarget(Class boxedDesired) { return boxedDesired == Date.class || boxedDesired == Instant.class || boxedDesired == LocalDateTime.class - || boxedDesired == LocalDate.class || boxedDesired == LocalTime.class - || boxedDesired == OffsetDateTime.class - || boxedDesired == ZonedDateTime.class; + || boxedDesired == LocalDate.class || boxedDesired == LocalTime.class + || boxedDesired == OffsetDateTime.class + || boxedDesired == ZonedDateTime.class; } private static Object convertToDateTime(Class target, Object value) { @@ -299,8 +334,8 @@ private static Object convertToDateTime(Class target, Object value) { } if (Temporal.class.isAssignableFrom(target) || target == LocalDate.class || target == LocalDateTime.class - || target == LocalTime.class || target == Instant.class || target == OffsetDateTime.class - || target == ZonedDateTime.class) { + || target == LocalTime.class || target == Instant.class || target == OffsetDateTime.class + || target == ZonedDateTime.class) { if (value instanceof Number) { long epoch = ((Number) value).longValue(); Instant inst = Instant.ofEpochMilli(epoch); @@ -311,12 +346,12 @@ private static Object convertToDateTime(Class target, Object value) { if (s.isEmpty()) return null; List> parsers = Arrays.asList( - Instant::parse, - OffsetDateTime::parse, - ZonedDateTime::parse, - LocalDateTime::parse, - LocalDate::parse, - LocalTime::parse); + Instant::parse, + OffsetDateTime::parse, + ZonedDateTime::parse, + LocalDateTime::parse, + LocalDate::parse, + LocalTime::parse); for (var p : parsers) { try { Object parsed = p.apply(s); @@ -326,11 +361,11 @@ private static Object convertToDateTime(Class target, Object value) { case "OffsetDateTime" -> convertInstantToTarget(target, ((OffsetDateTime) parsed).toInstant()); case "ZonedDateTime" -> convertInstantToTarget(target, ((ZonedDateTime) parsed).toInstant()); case "LocalDateTime" -> convertInstantToTarget(target, - ((LocalDateTime) parsed).atZone(ZoneId.systemDefault()).toInstant()); + ((LocalDateTime) parsed).atZone(ZoneId.systemDefault()).toInstant()); case "LocalDate" -> convertInstantToTarget(target, - ((LocalDate) parsed).atStartOfDay(ZoneId.systemDefault()).toInstant()); + ((LocalDate) parsed).atStartOfDay(ZoneId.systemDefault()).toInstant()); case "LocalTime" -> convertInstantToTarget(target, LocalDateTime - .of(LocalDate.now(), (LocalTime) parsed).atZone(ZoneId.systemDefault()).toInstant()); + .of(LocalDate.now(), (LocalTime) parsed).atZone(ZoneId.systemDefault()).toInstant()); default -> null; }; } catch (DateTimeParseException ignored) { diff --git a/core/src/test/java/com/euonia/reflection/DelegateServiceResolverTest.java b/core/src/test/java/com/euonia/reflection/DelegateServiceResolverTest.java new file mode 100644 index 0000000..a6451a2 --- /dev/null +++ b/core/src/test/java/com/euonia/reflection/DelegateServiceResolverTest.java @@ -0,0 +1,59 @@ +package com.euonia.reflection; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("DelegateServiceResolver") +class DelegateServiceResolverTest { + + @Test + @DisplayName("Given registered service when resolving then delegated instance is returned") + void givenRegisteredServiceWhenResolvingThenReturnDelegatedInstance() { + SampleService instance = new SampleService(); + DelegateServiceResolver resolver = new DelegateServiceResolver(type -> type == SampleService.class ? instance : null); + + assertSame(instance, resolver.getService(SampleService.class)); + assertNull(resolver.getService(NeedsArguments.class)); + } + + @Test + @DisplayName("Given matching constructor arguments when creating instance then object is constructed") + void givenMatchingConstructorArgumentsWhenCreatingInstanceThenReturnConstructedObject() { + DelegateServiceResolver resolver = new DelegateServiceResolver(type -> null); + + NeedsArguments instance = resolver.createInstance(NeedsArguments.class, "demo", 3); + + assertEquals("demo", instance.name); + assertEquals(3, instance.count); + } + + @Test + @DisplayName("Given no matching constructor when creating instance then illegal state is thrown") + void givenNoMatchingConstructorWhenCreatingInstanceThenThrowIllegalState() { + DelegateServiceResolver resolver = new DelegateServiceResolver(type -> null); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> resolver.createInstance(NeedsArguments.class, 42)); + + assertEquals("Could not construct type: " + NeedsArguments.class.getName(), exception.getMessage()); + } + + static class SampleService { + SampleService() { + } + } + + static class NeedsArguments { + private final String name; + private final int count; + + NeedsArguments(String name, int count) { + this.name = name; + this.count = count; + } + } +} From 628cc0b8bb419cd67f78ac5877fadfed179498a8 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 18:16:17 +0800 Subject: [PATCH 15/16] Enhance Pipeline interface with additional methods for component management and execution --- .../java/com/euonia/pipeline/Pipeline.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/pipeline/src/main/java/com/euonia/pipeline/Pipeline.java b/pipeline/src/main/java/com/euonia/pipeline/Pipeline.java index 92cdb01..a006d9f 100644 --- a/pipeline/src/main/java/com/euonia/pipeline/Pipeline.java +++ b/pipeline/src/main/java/com/euonia/pipeline/Pipeline.java @@ -4,20 +4,83 @@ import java.util.function.BiFunction; import java.util.function.Function; +/** + * Pipeline interface defines the contract for building and executing a pipeline + * of components that can process a context object. + * It allows adding components to the pipeline, building the pipeline, and + * running it asynchronously with a given context. + * The components can be added in a specific order or based on their type, and + * the pipeline can be executed with an optional accumulation function to + * combine results from multiple components. + */ public interface Pipeline { + /** + * Adds a component to the pipeline. The component is defined as a function that + * takes a PipelineDelegate and returns a PipelineDelegate. + * + * @param component The component to add to the pipeline. + * @return The current pipeline instance. + */ Pipeline use(Function component); + /** + * Adds a component to the pipeline at a specific index. The component is defined as a function that + * takes a PipelineDelegate and returns a PipelineDelegate. + * + * @param component The component to add to the pipeline. + * @param index The index at which to add the component. + * @return The current pipeline instance. + */ Pipeline use(Function component, int index); + /** + * Adds a component to the pipeline. The component is defined as a BiFunction that + * takes a context object and a PipelineDelegate, and returns a CompletionStage. + * + * @param handler The handler to add to the pipeline. + * @return The current pipeline instance. + */ Pipeline use(BiFunction> handler); + /** + * Adds a component to the pipeline based on its type. The component is defined as a class and optional arguments. + * + * @param type The class type of the component to add. + * @param args Optional arguments for the component's constructor. + * @return The current pipeline instance. + */ Pipeline use(Class type, Object... args); + /** + * Adds a component to the pipeline based on its context type. The component can be added ahead of others. + * + * @param contextType The context type of the component. + * @param useAheadOfOthers Whether to add the component ahead of others. + * @return The current pipeline instance. + */ Pipeline useOf(Class contextType, boolean useAheadOfOthers); + /** + * Builds the pipeline and returns a PipelineDelegate. + * + * @return The built PipelineDelegate. + */ PipelineDelegate build(); + /** + * Runs the pipeline asynchronously with the given context. + * + * @param context The context object to pass through the pipeline. + * @return A CompletionStage representing the asynchronous execution. + */ CompletionStage runAsync(Object context); + /** + * Runs the pipeline asynchronously with the given context and an accumulation function. + * + * @param context The context object to pass through the pipeline. + * @param accumulate The function to accumulate results from multiple components. + * @return A CompletionStage representing the asynchronous execution. + */ CompletionStage runAsync(Object context, Function> accumulate); } From 7308af1e3fcd68a7d7802ce5f2d0affc5ec3ad71 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 8 Jun 2026 18:16:43 +0800 Subject: [PATCH 16/16] Refactor isAssignable method to use TypeHelper for primitive type boxing --- .../euonia/reflection/ApplicationContextServiceResolver.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring/src/main/java/com/euonia/reflection/ApplicationContextServiceResolver.java b/spring/src/main/java/com/euonia/reflection/ApplicationContextServiceResolver.java index 03fe3a6..4865582 100644 --- a/spring/src/main/java/com/euonia/reflection/ApplicationContextServiceResolver.java +++ b/spring/src/main/java/com/euonia/reflection/ApplicationContextServiceResolver.java @@ -59,7 +59,8 @@ private static boolean isAssignable(Class[] parameterTypes, Object[] args) { continue; } - if (!parameterTypes[i].isAssignableFrom(arg.getClass())) { + Class parameterType = TypeHelper.boxIfPrimitive(parameterTypes[i]); + if (!parameterType.isAssignableFrom(arg.getClass())) { return false; } }