diff --git a/README.md b/README.md index 42d9af2..99b34a2 100644 --- a/README.md +++ b/README.md @@ -1 +1,279 @@ -# 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 + 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 +``` + +### 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 | + +### 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. + +#### 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, 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 +// 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") 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/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 new file mode 100644 index 0000000..beb6f37 --- /dev/null +++ b/core/src/main/java/com/euonia/reflection/ServiceResolver.java @@ -0,0 +1,57 @@ +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 new file mode 100644 index 0000000..ef82509 --- /dev/null +++ b/core/src/main/java/com/euonia/reflection/SimpleServiceResolver.java @@ -0,0 +1,89 @@ +package com.euonia.reflection; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +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<>(); + + public void register(Class type, T instance) { + services.put(type, instance); + } + + @Override + 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("Cannot 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; + } + 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/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/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())); + } } 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; + } + } +} 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 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 { } diff --git a/pipeline/README.md b/pipeline/README.md new file mode 100644 index 0000000..2123dc2 --- /dev/null +++ b/pipeline/README.md @@ -0,0 +1,414 @@ + +# Pipeline Module + +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 + public PipelineFactory pipelineFactory(ServiceResolver resolver) { + return new DefaultPipelineFactory(resolver); + } +} +``` + +### 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); +``` 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..3e4ea86 --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/DefaultPipelineProvider.java @@ -0,0 +1,87 @@ +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 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 (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/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..a006d9f --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/Pipeline.java @@ -0,0 +1,86 @@ +package com.euonia.pipeline; + +import java.util.concurrent.CompletionStage; +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); +} 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..e9f7f86 --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/PipelineBase.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.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) { + PipelineBehaviors annotation = contextType.getAnnotation(PipelineBehaviors.class); + if (annotation == null) { + return this; + } + if (useAheadOfOthers) { + 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 (Class behaviorType : annotation.value()) { + use(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/PipelineBehaviors.java b/pipeline/src/main/java/com/euonia/pipeline/PipelineBehaviors.java new file mode 100644 index 0000000..79dc753 --- /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 { + Class[] 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..c0da33c --- /dev/null +++ b/pipeline/src/main/java/com/euonia/pipeline/RequestResponsePipelineBase.java @@ -0,0 +1,93 @@ +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) { + PipelineBehaviors annotation = contextType.getAnnotation(PipelineBehaviors.class); + if (annotation == null) { + return this; + } + if (useAheadOfOthers) { + 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 (Class behaviorType : annotation.value()) { + use(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..7aad5b2 --- /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); + } + + @PipelineBehaviors(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); + } + } +} +*/ 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 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..4865582 --- /dev/null +++ b/spring/src/main/java/com/euonia/reflection/ApplicationContextServiceResolver.java @@ -0,0 +1,69 @@ +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; + } + + Class parameterType = TypeHelper.boxIfPrimitive(parameterTypes[i]); + if (!parameterType.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); + } +}