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 CallableProcessingInterceptorVersion: 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;
}
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 ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 3.2.9.RELEASE - 7.0.6 Version compatibilities of spring-webmvc with this example: 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
|