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:

  1. Declaration: A class implements IExtensionInterceptor and is marked with @Interceptor (meta-annotated with @Component), triggering component scanning.
  2. Registration: During bean registration, InterceptorDef.registerBean() extracts the real target object using InternalAopUtils.getTarget(bean) and calls InternalIndexer.index(this).
  3. Retrieval: When BaseRouter or DDD.java creates dynamic proxies for extension points, they retrieve the interceptor via InternalIndexer.registeredInterceptor().
  4. Invocation: ExtensionInvocationHandler.invokeExtension() constructs an ExtensionContext and calls beforeInvocation(context) before the method runs and afterInvocation(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) throws BootstrapException if 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 ThreadLocal or the ExtensionContext attribute 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 beforeInvocation prevent the extension method from executing entirely. Exceptions in afterInvocation occur after method completion and may override the original result or exception.

Summary

  • The cp-ddd-framework enforces a single IExtensionInterceptor instance, registered via @Interceptor and stored in InternalIndexer.
  • Implement beforeInvocation to execute logic before extension methods run, with access to modify arguments via ExtensionContext.getArgs().
  • Implement afterInvocation to execute cleanup, logging, or result inspection after extension completion.
  • The interceptor integrates with Spring's component scan through the @Interceptor annotation (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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →