Close

Spring MVC - Intercepting Async process lifecycle using CallableProcessingInterceptor

[Last Updated: Aug 19, 2026]

We saw in the last tutorial, using AsyncHandlerInterceptor was a way to intercept async processing.

To have more interceptions around user defined 'Callable' process, we can register CallableProcessingInterceptor

Definition of CallableProcessingInterceptor

Version: 7.0.6
 package org.springframework.web.context.request.async;
 public interface CallableProcessingInterceptor {
     default <T> void beforeConcurrentHandling(NativeWebRequest request, 1
                                         Callable<T> task)
                                         throws Exception;
     default <T> void preProcess(NativeWebRequest request, 2
                                 Callable<T> task)
                                 throws Exception;
     default <T> void postProcess(NativeWebRequest request, 3
                                  Callable<T> task,
                                  @Nullable Object concurrentResult)
                                  throws Exception;
     default <T> Object handleTimeout(NativeWebRequest request, 4
                                      Callable<T> task)
                                      throws Exception;
     default <T> Object handleError(NativeWebRequest request, 5
                                    Callable<T> task,
                                    Throwable t)
                                    throws Exception;
     default <T> void afterCompletion(NativeWebRequest request, 6
                                      Callable<T> task)
                                      throws Exception;
 }
1Invoked before the start of concurrent handling in the original thread in which the Callable is submitted for concurrent handling.
2Invoked after the start of concurrent handling in the async thread in which the Callable is executed and before the actual invocation of the Callable.
3Invoked after the Callable has produced a result in the async thread in which the Callable is executed.
4Invoked from a container thread when the async request times out before the Callable task completes.
5Invoked from a container thread when an error occurred while processing the async request before the Callable task completes. (Since 5.0)
6Invoked from a container thread when async processing completes for any reason including timeout or network error.

Example


Creating CallableProcessingInterceptor

package com.logicbig.example;

import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import java.time.LocalTime;
import java.util.concurrent.Callable;

public class MyCallableProcessingInterceptor implements
        CallableProcessingInterceptor {
    @Override
    public <T> void beforeConcurrentHandling(
            NativeWebRequest request,
            Callable<T> task) throws Exception {
        log("beforeConcurrentHandling called");
    }

    @Override
    public <T> void preProcess(
            NativeWebRequest request,
            Callable<T> task) throws Exception {

        log("preProcess called.");
    }

    @Override
    public <T> void postProcess(NativeWebRequest request,
                                Callable<T> task,
                                Object concurrentResult) throws Exception {
        log("postProcess called");
    }

    @Override
    public <T> Object handleTimeout(NativeWebRequest request,
                                    Callable<T> task) throws Exception {

        System.out.println("handleTimeout called.");

        return RESULT_NONE;
    }

    @Override
    public <T> void afterCompletion(NativeWebRequest request,
                                    Callable<T> task) throws Exception {
        log("afterCompletion called.");
    }

    private static void log(String msg) {
        System.out.printf("%s CallableInterceptor [%s] - %s%n",
                          LocalTime.now(),
                          Thread.currentThread().getName(),
                          msg);
    }
}


Registering the interceptor

We can register the above interceptor in the HandlerInterceptor#preHandle method on the fly. We are going to modify our previous example MyAsyncHandlerInterceptor a little for that:

package com.logicbig.example;

import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
import java.time.LocalTime;
import java.util.concurrent.Callable;

public class MyCallableProcessingInterceptor implements
        CallableProcessingInterceptor {
    @Override
    public <T> void beforeConcurrentHandling(
            NativeWebRequest request,
            Callable<T> task) throws Exception {
        log("beforeConcurrentHandling called");
    }

    @Override
    public <T> void preProcess(
            NativeWebRequest request,
            Callable<T> task) throws Exception {

        log("preProcess called.");
    }

    @Override
    public <T> void postProcess(NativeWebRequest request,
                                Callable<T> task,
                                Object concurrentResult) throws Exception {
        log("postProcess called");
    }

    @Override
    public <T> Object handleTimeout(NativeWebRequest request,
                                    Callable<T> task) throws Exception {

        System.out.println("handleTimeout called.");

        return RESULT_NONE;
    }

    @Override
    public <T> void afterCompletion(NativeWebRequest request,
                                    Callable<T> task) throws Exception {
        log("afterCompletion called.");
    }

    private static void log(String msg) {
        System.out.printf("%s CallableInterceptor [%s] - %s%n",
                          LocalTime.now(),
                          Thread.currentThread().getName(),
                          msg);
    }
}

Running the example

To try examples, run embedded Jetty (configured in pom.xml of example project below):

mvn jetty:run

Using curl

$ curl -s "http://localhost:8080/test"
async result

Server Output


14:20:28.757442700 Interceptor [qtp1861083998-39] DispatcherType: REQUEST - preHandle called.
14:20:28.779139 Controller [qtp1861083998-39] - handler called.
14:20:28.780123900 Controller [qtp1861083998-39] - handler finished
14:20:28.785127300 CallableInterceptor [qtp1861083998-39] - beforeConcurrentHandling called
14:20:28.789126100 CallableInterceptor [MvcAsync1] - preProcess called.
14:20:28.790255200 Controller [MvcAsync1] - callable#async task started.
14:20:28.790255200 Interceptor [qtp1861083998-39] DispatcherType: REQUEST - afterConcurrentHandlingStarted called.
14:20:29.294265 Controller [MvcAsync1] - callable#async task finished
14:20:29.294265 CallableInterceptor [MvcAsync1] - postProcess called
14:20:29.300365 Interceptor [qtp1861083998-43] DispatcherType: ASYNC - preHandle called.
14:20:29.318262700 Interceptor [qtp1861083998-43] DispatcherType: ASYNC - postHandle called.
14:20:29.319262800 Interceptor [qtp1861083998-43] DispatcherType: ASYNC - afterCompletion called.
14:20:29.327684500 CallableInterceptor [qtp1861083998-43] - afterCompletion called.

Here we can see that CallableProcessingInterceptor is integrated more deeply with the lifecycle of an asynchronous request.

Here's the flow diagram to see a clear difference from the last example:


Integration Test

package com.logicbig.example;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;

@SpringJUnitWebConfig(WebConfig.class)
public class ControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvcTester mockMvcTester;

    @BeforeEach
    public void setup() {
        this.mockMvcTester = MockMvcTester.from(this.wac);
    }

    @Test
    public void testController() {
        assertThat(this.mockMvcTester.get().uri("/test").exchange())
                .hasStatusOk()
                .bodyText().isEqualTo("async result");
    }
}
mvn clean test -Dtest="ControllerTest"

Output

$ mvn clean test -Dtest="ControllerTest"
[INFO] Scanning for projects...
[INFO]
[INFO] ------< com.logicbig.example:async-callable-process-interceptor >-------
[INFO] Building async-callable-process-interceptor 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ async-callable-process-interceptor ---
[INFO] Deleting D:\example-projects\spring-mvc\async-callable-process-interceptor\target
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ async-callable-process-interceptor ---
[INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\async-callable-process-interceptor\src\main\resources
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ async-callable-process-interceptor ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 5 source files with javac [debug target 25] to target\classes
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ async-callable-process-interceptor ---
[INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\async-callable-process-interceptor\src\test\resources
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ async-callable-process-interceptor ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 2 source files with javac [debug target 25] to target\test-classes
[INFO]
[INFO] --- surefire:3.2.5:test (default-test) @ async-callable-process-interceptor ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.logicbig.example.ControllerTest
15:04:24.187720300 Interceptor [main] DispatcherType: REQUEST - preHandle called.
15:04:24.198899 Controller [main] - handler called.
15:04:24.199906700 Controller [main] - handler finished
15:04:24.203913500 CallableInterceptor [main] - beforeConcurrentHandling called
15:04:24.205911900 Interceptor [main] DispatcherType: REQUEST - afterConcurrentHandlingStarted called.
15:04:24.206891100 CallableInterceptor [MvcAsync1] - preProcess called.
15:04:24.206891100 Controller [MvcAsync1] - callable#async task started.
15:04:24.709060400 Controller [MvcAsync1] - callable#async task finished
15:04:24.709060400 CallableInterceptor [MvcAsync1] - postProcess called
15:04:24.710971200 Interceptor [main] DispatcherType: ASYNC - preHandle called.
15:04:24.721972500 Interceptor [main] DispatcherType: ASYNC - postHandle called.
15:04:24.721972500 Interceptor [main] DispatcherType: ASYNC - afterCompletion called.
15:04:24.722988200 CallableInterceptor [main] - afterCompletion called.
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.818 s -- in com.logicbig.example.ControllerTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 8.853 s
[INFO] Finished at: 2026-08-18T15:04:24+08:00
[INFO] ------------------------------------------------------------------------
INFO: Completed initialization in 2 ms

Example Project

Dependencies and Technologies Used:

  • spring-webmvc 7.0.6 (Spring Web MVC)
     Version Compatibility: 3.2.9.RELEASE - 7.0.6Version List
    ×

    Version compatibilities of spring-webmvc with this example:

      javax.servlet-api:3.x
    • 3.2.9.RELEASE
    • 3.2.10.RELEASE
    • 3.2.11.RELEASE
    • 3.2.12.RELEASE
    • 3.2.13.RELEASE
    • 3.2.14.RELEASE
    • 3.2.15.RELEASE
    • 3.2.16.RELEASE
    • 3.2.17.RELEASE
    • 3.2.18.RELEASE
    • 4.0.0.RELEASE
    • 4.0.1.RELEASE
    • 4.0.2.RELEASE
    • 4.0.3.RELEASE
    • 4.0.4.RELEASE
    • 4.0.5.RELEASE
    • 4.0.6.RELEASE
    • 4.0.7.RELEASE
    • 4.0.8.RELEASE
    • 4.0.9.RELEASE
    • 4.1.0.RELEASE
    • 4.1.1.RELEASE
    • 4.1.2.RELEASE
    • 4.1.3.RELEASE
    • 4.1.4.RELEASE
    • 4.1.5.RELEASE
    • 4.1.6.RELEASE
    • 4.1.7.RELEASE
    • 4.1.8.RELEASE
    • 4.1.9.RELEASE
    • 4.2.0.RELEASE
    • 4.2.1.RELEASE
    • 4.2.2.RELEASE
    • 4.2.3.RELEASE
    • 4.2.4.RELEASE
    • 4.2.5.RELEASE
    • 4.2.6.RELEASE
    • 4.2.7.RELEASE
    • 4.2.8.RELEASE
    • 4.2.9.RELEASE
    • 4.3.0.RELEASE
    • 4.3.1.RELEASE
    • 4.3.2.RELEASE
    • 4.3.3.RELEASE
    • 4.3.4.RELEASE
    • 4.3.5.RELEASE
    • 4.3.6.RELEASE
    • 4.3.7.RELEASE
    • 4.3.8.RELEASE
    • 4.3.9.RELEASE
    • 4.3.10.RELEASE
    • 4.3.11.RELEASE
    • 4.3.12.RELEASE
    • 4.3.13.RELEASE
    • 4.3.14.RELEASE
    • 4.3.15.RELEASE
    • 4.3.16.RELEASE
    • 4.3.17.RELEASE
    • 4.3.18.RELEASE
    • 4.3.19.RELEASE
    • 4.3.20.RELEASE
    • 4.3.21.RELEASE
    • 4.3.22.RELEASE
    • 4.3.23.RELEASE
    • 4.3.24.RELEASE
    • 4.3.25.RELEASE
    • 4.3.26.RELEASE
    • 4.3.27.RELEASE
    • 4.3.28.RELEASE
    • 4.3.29.RELEASE
    • 4.3.30.RELEASE
    • 5.0.0.RELEASE
    • 5.0.1.RELEASE
    • 5.0.2.RELEASE
    • 5.0.3.RELEASE
    • 5.0.4.RELEASE
    • 5.0.5.RELEASE
    • 5.0.6.RELEASE
    • 5.0.7.RELEASE
    • 5.0.8.RELEASE
    • 5.0.9.RELEASE
    • 5.0.10.RELEASE
    • 5.0.11.RELEASE
    • 5.0.12.RELEASE
    • 5.0.13.RELEASE
    • 5.0.14.RELEASE
    • 5.0.15.RELEASE
    • 5.0.16.RELEASE
    • 5.0.17.RELEASE
    • 5.0.18.RELEASE
    • 5.0.19.RELEASE
    • 5.0.20.RELEASE
    • 5.1.0.RELEASE
    • 5.1.1.RELEASE
    • 5.1.2.RELEASE
    • 5.1.3.RELEASE
    • 5.1.4.RELEASE
    • 5.1.5.RELEASE
    • 5.1.6.RELEASE
    • 5.1.7.RELEASE
    • 5.1.8.RELEASE
    • 5.1.9.RELEASE
    • 5.1.10.RELEASE
    • 5.1.11.RELEASE
    • 5.1.12.RELEASE
    • 5.1.13.RELEASE
    • 5.1.14.RELEASE
    • 5.1.15.RELEASE
    • 5.1.16.RELEASE
    • 5.1.17.RELEASE
    • 5.1.18.RELEASE
    • 5.1.19.RELEASE
    • 5.1.20.RELEASE
    • 5.2.0.RELEASE
    • 5.2.1.RELEASE
    • 5.2.2.RELEASE
    • 5.2.3.RELEASE
    • 5.2.4.RELEASE
    • 5.2.5.RELEASE
    • 5.2.6.RELEASE
    • 5.2.7.RELEASE
    • 5.2.8.RELEASE
    • 5.2.9.RELEASE
    • 5.2.10.RELEASE
    • 5.2.11.RELEASE
    • 5.2.12.RELEASE
    • 5.2.13.RELEASE
    • 5.2.14.RELEASE
    • 5.2.15.RELEASE
    • 5.2.16.RELEASE
    • 5.2.17.RELEASE
    • 5.2.18.RELEASE
    • 5.2.19.RELEASE
    • 5.2.20.RELEASE
    • 5.2.21.RELEASE
    • 5.2.22.RELEASE
    • 5.2.23.RELEASE
    • 5.2.24.RELEASE
    • 5.2.25.RELEASE
    • 5.3.0
    • 5.3.1
    • 5.3.2
    • 5.3.3
    • 5.3.4
    • javax.servlet-api:4.x
    • 5.3.5
    • 5.3.6
    • 5.3.7
    • 5.3.8
    • 5.3.9
    • 5.3.10
    • 5.3.11
    • 5.3.12
    • 5.3.13
    • 5.3.14
    • 5.3.15
    • 5.3.16
    • 5.3.17
    • 5.3.18
    • 5.3.19
    • 5.3.20
    • 5.3.21
    • 5.3.22
    • 5.3.23
    • 5.3.24
    • 5.3.25
    • 5.3.26
    • 5.3.27
    • 5.3.28
    • 5.3.29
    • 5.3.30
    • 5.3.31
    • 5.3.32
    • 5.3.33
    • 5.3.34
    • 5.3.35
    • 5.3.36
    • 5.3.37
    • 5.3.38
    • 5.3.39
    • javax.* -> jakarta.*
      jakarta.servlet-api:6.x
      Java 17 min
    • 6.0.0
    • 6.0.1
    • 6.0.2
    • 6.0.3
    • 6.0.4
    • 6.0.5
    • 6.0.6
    • 6.0.7
    • 6.0.8
    • 6.0.9
    • 6.0.10
    • 6.0.11
    • 6.0.12
    • 6.0.13
    • 6.0.14
    • 6.0.15
    • 6.0.16
    • 6.0.17
    • 6.0.18
    • 6.0.19
    • 6.0.20
    • 6.0.21
    • 6.0.22
    • 6.0.23
    • 6.1.0
    • 6.1.1
    • 6.1.2
    • 6.1.3
    • 6.1.4
    • 6.1.5
    • 6.1.6
    • 6.1.7
    • 6.1.8
    • 6.1.9
    • 6.1.10
    • 6.1.11
    • 6.1.12
    • 6.1.13
    • 6.1.14
    • 6.1.15
    • 6.1.16
    • 6.1.17
    • 6.1.18
    • 6.1.19
    • 6.1.20
    • 6.1.21
    • 6.2.0
    • 6.2.1
    • 6.2.2
    • 6.2.3
    • 6.2.4
    • 6.2.5
    • 6.2.6
    • 6.2.7
    • 6.2.8
    • 6.2.9
    • 6.2.10
    • 6.2.11
    • 6.2.12
    • 6.2.13
    • 6.2.14
    • 6.2.15
    • 6.2.16
    • 6.2.17
    • 6.2.18
    • 6.2.19
    • 7.0.0
    • 7.0.1
    • 7.0.2
    • 7.0.3
    • 7.0.4
    • 7.0.5
    • 7.0.6

    Versions in green have been tested.

  • jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
  • spring-test 7.0.6 (Spring TestContext Framework)
  • junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
  • hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
  • assertj-core 3.26.3 (Rich and fluent assertions for testing in Java)
  • JDK 25
  • Maven 3.9.11

Spring MVC - CallableProcessingInterceptor Example Select All Download
  • async-callable-process-interceptor
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • MyCallableProcessingInterceptor.java
        • test
          • java
            • com
              • logicbig
                • example

    See Also

    Join