How to Implement a Custom IExtensionInterceptor in cp-ddd-framework: Complete Guide
Implementing a custom IExtensionInterceptor requires creating a Spring bean annotated with @Interceptor that implements beforeInvocation and afterInvocation methods to modify extension behavior, with the framework enforcing a singleton interceptor through InternalIndexer.
The cp-ddd-framework (funkygao/cp-ddd-framework) provides a powerful interception mechanism for domain extension points through the IExtensionInterceptor interface. When implementing custom IExtensionInterceptor logic, developers can inject cross-cutting concerns such as logging, security checks, or argument transformation before and after every extension method invocation.
Understanding the IExtensionInterceptor Architecture
The framework's interception mechanism is built around a singleton interceptor pattern enforced in InternalIndexer.java. The lifecycle follows four distinct phases wired into the Spring context:
- Declaration: A class implements
IExtensionInterceptorand is marked with@Interceptor(meta-annotated with@Component), triggering component scanning. - Registration: During bean registration,
InterceptorDef.registerBean()extracts the real target object usingInternalAopUtils.getTarget(bean)and callsInternalIndexer.index(this). - Retrieval: When
BaseRouterorDDD.javacreates dynamic proxies for extension points, they retrieve the interceptor viaInternalIndexer.registeredInterceptor(). - Invocation:
ExtensionInvocationHandler.invokeExtension()constructs anExtensionContextand callsbeforeInvocation(context)before the method runs andafterInvocation(context)afterward.
Because InternalIndexer.index(InterceptorDef) throws BootstrapException if a second registration is attempted, you must consolidate all cross-cutting concerns into a single interceptor bean.
Step-by-Step Implementation Guide
Step 1: Create the Interceptor Class
Define a class implementing IExtensionInterceptor and annotate it with @Interceptor to enable Spring component scanning:
package com.example.interceptor;
import io.github.dddplus.annotation.Interceptor;
import io.github.dddplus.runtime.interceptor.ExtensionContext;
import io.github.dddplus.runtime.interceptor.IExtensionInterceptor;
import lombok.NonNull;
@Interceptor
public class MyExtensionInterceptor implements IExtensionInterceptor {
@Override
public void beforeInvocation(@NonNull ExtensionContext ctx) {
// Pre-processing logic
}
@Override
public void afterInvocation(@NonNull ExtensionContext ctx) {
// Post-processing logic
}
}
Step 2: Implement beforeInvocation Logic
The beforeInvocation method receives an ExtensionContext containing the extension code, target bean, reflected Method, and arguments array. You can modify the args array directly to alter the method invocation:
@Override
public void beforeInvocation(@NonNull ExtensionContext ctx) {
// Access extension metadata
String extensionCode = ctx.getCode();
Object extensionInstance = ctx.getExtension();
Method method = ctx.getMethod();
Object[] args = ctx.getArgs();
// Modify arguments (e.g., trim strings)
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof String) {
args[i] = ((String) args[i]).trim();
}
}
}
Step 3: Implement afterInvocation Logic
The afterInvocation method executes after the extension method completes, regardless of success or failure. Use this for cleanup, logging, or result modification:
@Override
public void afterInvocation(@NonNull ExtensionContext ctx) {
// Log completion
System.out.println("Extension " + ctx.getCode() + " execution completed");
// Note: If the method threw an exception, it is available via context
// You can also modify the return value by manipulating the context if needed
}
Practical Implementation Examples
Logging and MDC Propagation
This example demonstrates implementing a custom IExtensionInterceptor for distributed tracing using SLF4J MDC:
package io.github.myapp.interceptor;
import io.github.dddplus.annotation.Interceptor;
import io.github.dddplus.runtime.interceptor.ExtensionContext;
import io.github.dddplus.runtime.interceptor.IExtensionInterceptor;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import java.util.UUID;
@Interceptor
@Slf4j
public class RequestIdInterceptor implements IExtensionInterceptor {
@Override
public void beforeInvocation(@NonNull ExtensionContext ctx) {
String requestId = MDC.get("requestId");
if (requestId == null) {
requestId = UUID.randomUUID().toString();
MDC.put("requestId", requestId);
}
log.info(">>> EXT {}.{} [code={}]",
ctx.getExtension().getClass().getSimpleName(),
ctx.getMethod().getName(),
ctx.getCode());
}
@Override
public void afterInvocation(@NonNull ExtensionContext ctx) {
MDC.remove("requestId");
log.info("<<< EXT {} completed", ctx.getCode());
}
}
Argument Normalization
This example shows how to sanitize inputs by modifying the args array directly:
package io.github.myapp.interceptor;
import io.github.dddplus.annotation.Interceptor;
import io.github.dddplus.runtime.interceptor.ExtensionContext;
import io.github.dddplus.runtime.interceptor.IExtensionInterceptor;
import lombok.NonNull;
@Interceptor
public class ArgumentNormalizer implements IExtensionInterceptor {
@Override
public void beforeInvocation(@NonNull ExtensionContext ctx) {
Object[] args = ctx.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof String) {
args[i] = ((String) args[i]).trim();
}
}
}
@Override
public void afterInvocation(@NonNull ExtensionContext ctx) {
// No post-processing required
}
}
Performance Monitoring
This example tracks execution duration using context attributes:
package io.github.myapp.interceptor;
import io.github.dddplus.annotation.Interceptor;
import io.github.dddplus.runtime.interceptor.ExtensionContext;
import io.github.dddplus.runtime.interceptor.IExtensionInterceptor;
import lombok.NonNull;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Interceptor
public class TimingInterceptor implements IExtensionInterceptor {
private static final ConcurrentMap<String, Long> timings = new ConcurrentHashMap<>();
@Override
public void beforeInvocation(@NonNull ExtensionContext ctx) {
ctx.setAttribute("startTs", System.nanoTime());
}
@Override
public void afterInvocation(@NonNull ExtensionContext ctx) {
Long start = (Long) ctx.removeAttribute("startTs");
if (start != null) {
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
timings.put(ctx.getCode(), elapsedMs);
}
}
public static long getLastTiming(String code) {
return timings.getOrDefault(code, -1L);
}
}
Critical Implementation Considerations
When implementing a custom IExtensionInterceptor in the cp-ddd-framework, keep these architectural constraints in mind:
-
Singleton enforcement: The framework strictly allows only one interceptor instance.
InternalIndexer.index(InterceptorDef)throwsBootstrapExceptionif a second registration is attempted. You must consolidate all cross-cutting concerns into a single class or delegate to internal components. -
Thread safety: The interceptor is invoked on the calling thread (unless a timeout configuration forces a thread switch, which occurs after the interceptor runs). Avoid mutable shared state; use
ThreadLocalor theExtensionContextattribute map for request-scoped data. -
Argument mutation: The
ExtensionContext.getArgs()method returns the actual argument array that will be passed to the extension method's reflection call. Modifications directly affect the invocation, enabling input validation or transformation. -
Exception handling: Exceptions thrown in
beforeInvocationprevent the extension method from executing entirely. Exceptions inafterInvocationoccur after method completion and may override the original result or exception.
Summary
- The cp-ddd-framework enforces a single
IExtensionInterceptorinstance, registered via@Interceptorand stored inInternalIndexer. - Implement
beforeInvocationto execute logic before extension methods run, with access to modify arguments viaExtensionContext.getArgs(). - Implement
afterInvocationto execute cleanup, logging, or result inspection after extension completion. - The interceptor integrates with Spring's component scan through the
@Interceptorannotation (meta-annotated with@Component). - All cross-cutting concerns must be consolidated into one interceptor bean due to the singleton constraint enforced in
InternalIndexer.index().
Frequently Asked Questions
How many IExtensionInterceptor instances can exist in a single application?
The framework enforces a strict singleton pattern allowing only one IExtensionInterceptor instance per application. InternalIndexer.index(InterceptorDef) throws a BootstrapException if a second interceptor bean is detected during startup. You must consolidate all interception logic into a single class or use delegation patterns within that single bean to handle multiple concerns.
Can I modify the arguments passed to an extension method?
Yes. The ExtensionContext.getArgs() method returns the actual argument array that will be passed to the extension method via reflection. By modifying elements of this array in beforeInvocation, you directly alter the input parameters before execution. This enables preprocessing patterns like string trimming, null checks, or argument wrapping.
What happens if my interceptor throws an exception?
If beforeInvocation throws an exception, the framework aborts the extension method invocation entirely and propagates the exception to the caller. If afterInvocation throws an exception, it occurs after the extension method has completed (successfully or with an exception), and this new exception may mask the original result or error. Implement robust error handling within your interceptor to avoid unintended side effects.
Is the IExtensionInterceptor thread-safe?
The interceptor instance itself is a singleton shared across all threads, but the framework invokes beforeInvocation and afterInvocation on the calling thread (unless a timeout configuration forces a thread switch, which happens after the interceptor runs). For thread-safe implementations, avoid mutable shared state or use ThreadLocal storage for request-scoped data. The ExtensionContext provides attribute storage via setAttribute and getAttribute for temporary state between before and after phases.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →