# How to Implement a Custom IExtensionInterceptor in cp-ddd-framework: Complete Guide

> Learn to implement a custom IExtensionInterceptor in cp-ddd-framework. Modify extension behavior by creating a Spring bean with beforeInvocation and afterInvocation methods. Get the complete guide.

- Repository: [Funky Gao/cp-ddd-framework](https://github.com/funkygao/cp-ddd-framework)
- Tags: how-to-guide
- Published: 2026-03-02

---

**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`](https://github.com/funkygao/cp-ddd-framework/blob/main/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`](https://github.com/funkygao/cp-ddd-framework/blob/main/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:

```java
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:

```java
@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:

```java
@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:

```java
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:

```java
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:

```java
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.