We saw in the last tutorial, how to use CallableProcessingInterceptor to integrate more deeply with async processing lifecycle.
To have similar lifecycle interception around user defined DeferredResult processing, we can register DeferredResultProcessingInterceptor.
Definition of DeferredResultProcessingInterceptorVersion: 7.0.6 package org.springframework.web.context.request.async;
public interface DeferredResultProcessingInterceptor {
default <T> void beforeConcurrentHandling(NativeWebRequest request, 1
DeferredResult<T> deferredResult)
throws Exception;
default <T> void preProcess(NativeWebRequest request, 2
DeferredResult<T> deferredResult)
throws Exception;
default <T> void postProcess(NativeWebRequest request, 3
DeferredResult<T> deferredResult,
@Nullable Object concurrentResult)
throws Exception;
default <T> boolean handleTimeout(NativeWebRequest request, 4
DeferredResult<T> deferredResult)
throws Exception;
default <T> boolean handleError(NativeWebRequest request, 5
DeferredResult<T> deferredResult,
Throwable t)
throws Exception;
default <T> void afterCompletion(NativeWebRequest request, 6
DeferredResult<T> deferredResult)
throws Exception;
}
Example
Creating the DeferredResultProcessingInterceptor
package com.logicbig.example;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor;
import java.time.LocalTime;
public class MyDeferredResultProcessingInterceptor implements
DeferredResultProcessingInterceptor {
@Override
public <T> void beforeConcurrentHandling(NativeWebRequest request,
DeferredResult<T> deferredResult)
throws Exception {
log("beforeConcurrentHandler called");
}
@Override
public <T> void preProcess(NativeWebRequest request,
DeferredResult<T> deferredResult) throws Exception {
log("preProcess called");
}
@Override
public <T> void postProcess(NativeWebRequest request,
DeferredResult<T> deferredResult,
Object concurrentResult) throws Exception {
log("postProcess called");
}
@Override
public <T> boolean handleTimeout(NativeWebRequest request,
DeferredResult<T> deferredResult) throws Exception {
log("handleTimeout called");
return false;
}
@Override
public <T> void afterCompletion(NativeWebRequest request,
DeferredResult<T> deferredResult) throws Exception {
log("afterCompletion called");
}
private static void log(String msg) {
System.out.printf("%s DeferredResultInterceptor [%s] - %s%n",
LocalTime.now(),
Thread.currentThread().getName(),
msg);
}
}
Registering the interceptor
Similar to our last tutorial example, We can register the above interceptor in the HandlerInterceptor#preHandle method using WebAsyncManager:
package com.logicbig.example;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor;
import java.time.LocalTime;
public class MyDeferredResultProcessingInterceptor implements
DeferredResultProcessingInterceptor {
@Override
public <T> void beforeConcurrentHandling(NativeWebRequest request,
DeferredResult<T> deferredResult)
throws Exception {
log("beforeConcurrentHandler called");
}
@Override
public <T> void preProcess(NativeWebRequest request,
DeferredResult<T> deferredResult) throws Exception {
log("preProcess called");
}
@Override
public <T> void postProcess(NativeWebRequest request,
DeferredResult<T> deferredResult,
Object concurrentResult) throws Exception {
log("postProcess called");
}
@Override
public <T> boolean handleTimeout(NativeWebRequest request,
DeferredResult<T> deferredResult) throws Exception {
log("handleTimeout called");
return false;
}
@Override
public <T> void afterCompletion(NativeWebRequest request,
DeferredResult<T> deferredResult) throws Exception {
log("afterCompletion called");
}
private static void log(String msg) {
System.out.printf("%s DeferredResultInterceptor [%s] - %s%n",
LocalTime.now(),
Thread.currentThread().getName(),
msg);
}
}
The Controller:
package com.logicbig.example;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.DeferredResult;
import java.time.LocalTime;
@Controller
public class MyWebController {
@GetMapping("/test")
@ResponseBody
public DeferredResult<String> handleTestRequest() {
log("handler called");
final DeferredResult<String> deferredResult = new DeferredResult<>();
new Thread(() -> {
log("deferred#async task started");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
deferredResult.setResult("test async result");
log("deferred#async task finished");
}).start();
log("handler finished");
return deferredResult;
}
private static void log(String msg) {
System.out.printf("%s Controller [%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 test async result
Server Output 18:11:41.495545200 Interceptor [qtp106193777-39] DispatcherType: REQUEST - preHandle called 18:11:41.511560900 Controller [qtp106193777-39] - handler called 18:11:41.511560900 Controller [qtp106193777-39] - handler finished 18:11:41.512571400 Controller [Thread-11] - deferred#async task started 18:11:41.516476 DeferredResultInterceptor [qtp106193777-39] - beforeConcurrentHandler called 18:11:41.518473100 DeferredResultInterceptor [qtp106193777-39] - preProcess called 18:11:41.519475200 Interceptor [qtp106193777-39] DispatcherType: REQUEST - afterConcurrentHandlingStarted called 18:11:41.814437900 DeferredResultInterceptor [Thread-11] - postProcess called 18:11:41.815716200 Controller [Thread-11] - deferred#async task finished 18:11:41.818727400 Interceptor [qtp106193777-40] DispatcherType: ASYNC - preHandle called 18:11:41.844731300 Interceptor [qtp106193777-40] DispatcherType: ASYNC - postHandle called 18:11:41.844731300 Interceptor [qtp106193777-40] DispatcherType: ASYNC - afterCompletion called 18:11:41.848188900 DeferredResultInterceptor [qtp106193777-40] - afterCompletion called
This output is similar to the last example. In this case, the user is responsible to create his own thread for async processing in the controller method. User would normally call DeferredResult.setResult() at the end of the processing, ` which will cause DeferredInterceptor#postProcess invocation. To understand the all interception points, here's the flow diagram:
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("test async result");
}
}
mvn clean test -Dtest="ControllerTest" Output$ mvn clean test -Dtest="ControllerTest" [INFO] Scanning for projects... [INFO] [INFO] ------< com.logicbig.example:async-deferred-process-interceptor >------- [INFO] Building async-deferred-process-interceptor 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- clean:3.2.0:clean (default-clean) @ async-deferred-process-interceptor --- [INFO] Deleting D:\example-projects\spring-mvc\async-deferred-process-interceptor\target [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ async-deferred-process-interceptor --- [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\async-deferred-process-interceptor\src\main\resources [INFO] [INFO] --- compiler:3.15.0:compile (default-compile) @ async-deferred-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-deferred-process-interceptor --- [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\async-deferred-process-interceptor\src\test\resources [INFO] [INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ async-deferred-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-deferred-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 10:48:00.676240800 Interceptor [main] DispatcherType: REQUEST - preHandle called 10:48:00.687238700 Controller [main] - handler called 10:48:00.688241600 Controller [main] - handler finished 10:48:00.689238500 Controller [Thread-1] - deferred#async task started 10:48:00.692240200 DeferredResultInterceptor [main] - beforeConcurrentHandler called 10:48:00.693239400 DeferredResultInterceptor [main] - preProcess called 10:48:00.693239400 Interceptor [main] DispatcherType: REQUEST - afterConcurrentHandlingStarted called 10:48:00.990641100 DeferredResultInterceptor [Thread-1] - postProcess called 10:48:00.991654300 Controller [Thread-1] - deferred#async task finished 10:48:00.991654300 Interceptor [main] DispatcherType: ASYNC - preHandle called 10:48:01.013337900 Interceptor [main] DispatcherType: ASYNC - postHandle called 10:48:01.013337900 Interceptor [main] DispatcherType: ASYNC - afterCompletion called 10:48:01.013337900 DeferredResultInterceptor [main] - afterCompletion called [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.338 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: 5.509 s [INFO] Finished at: 2026-08-19T10:48:01+08:00 [INFO] ------------------------------------------------------------------------ INFO: Completed initialization in 1 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
|