Close

Spring MVC - Intercepting Async Requests using AsyncHandlerInterceptor

[Last Updated: Aug 16, 2026]

Spring extends the concept of intercepting requests when it comes to asynchronous processing.

Instead of using HandlerInterceptor, the interface AsyncHandlerInterceptor implementations can be used to intercept request involving async processing.

AsyncHandlerInterceptor defines one method

void afterConcurrentHandlingStarted(HttpServletRequest request,
                                    HttpServletResponse response,
                                    Object handler)
                             throws Exception

AsyncHandlerInterceptor is a sub-interface of HandlerInterceptor.


Example


Creating the interceptor

package com.logicbig.example;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import java.time.LocalTime;

public class MyAsyncHandlerInterceptor
        implements AsyncHandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {

        log("interceptor#preHandle", request);
        return true;

    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {

        log("interceptor#postHandle", request);
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {

        log("interceptor#afterCompletion", request);
    }

    @Override
    public void afterConcurrentHandlingStarted(HttpServletRequest request,
                                               HttpServletResponse response,
                                               Object handler) throws Exception {

        log("interceptor#afterConcurrentHandlingStarted", request);
    }

    private static void log(String stage,
                            HttpServletRequest request) {
        System.out.printf("%s %s called. "
                                  + "Thread: %s, "
                                  + "DispatcherType: %s%n",
                          LocalTime.now(),
                          stage,
                          Thread.currentThread().getName(),
                          request.getDispatcherType());
    }
}

Creating the controller

package com.logicbig.example;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.time.LocalTime;
import java.util.concurrent.Callable;

@Controller
public class MyController {
    @RequestMapping("/test")
    @ResponseBody
    public Callable<String> handleTestRequest() {

        log("controller#handler called");

        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                log("controller-callable#async task started");
                Thread.sleep(300);
                log("controller-callable#async task finished");
                return "async result";
            }
        };

        log("controller#handler finished");
        return callable;
    }

    private static void log(String msg) {
        System.out.println(
                LocalTime.now() +
                        " MyController [" +
                        Thread.currentThread().getName() + "] "
                        + msg);
    }
}


Config class

package com.logicbig.example;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
@EnableWebMvc
@ComponentScan
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyAsyncHandlerInterceptor());
    }

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setThreadNamePrefix("mvc-async-");
        executor.initialize();
        configurer.setTaskExecutor(executor);
    }
}

Running the application

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

mvn jetty:run

Access the application on the browser, we will have this output:


On server console we have this output related to our request processing:


The main request thread should not be blocked for a long time. It should exit immediately after starting a new thread in the handler method. The new thread will perform some application specific long running task.

The method AsyncHandlerInterceptor#afterConcurrentHandlingStarted is called after a new thread for async process (Callable) has been created. This interceptor call is made in the same request thread.

afterConcurrentHandlingStarted invocation is a chance for developer to perform tasks such as cleaning up thread-bound attributes before releasing the thread to the Servlet container.

Just after completing user defined async processing, new series of calls of preHandler(), postHandler() and afterCompletion() are made in a new thread but on the same instance of the interceptor. Since this interception happens during the final response, the developer can apply application logic like login etc as we saw in the HandlerInterceptor tutorial.

To understand what's going on, here is the flow diagram:


Note that the method afterConcurrentHandlingStarted might get called more or less same time when async processing starts in the new thread. This call might be before or after the async processing starts, that's due to thread scheduling differences, but it shouldn't effect our logic which we are supposed to apply in afterConcurrentHandlingStarted.

Within interceptor, to distinguish between the initial request and the subsequent dispatch after asynchronous handling completes, interceptors can check whether the javax.servlet.DispatcherType returns from ServletRequest#getDispatcherType() call is "REQUEST" or "ASYNC".

Also note that we used Callable approach for long running task in the above example. We can also use DeferredResult approach as well with the same interceptor. Please see examples here


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(mockMvcTester.get()
                                .uri("/test"))
                .hasStatusOk()
                .hasBodyTextEqualTo("async result");

    }
}
mvn clean test -Dtest="ControllerTest"

Output

$ mvn clean test -Dtest="ControllerTest"
[INFO] Scanning for projects...
[INFO]
[INFO] -------< com.logicbig.example:async-handler-interceptor-example >-------
[INFO] Building async-handler-interceptor-example 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ async-handler-interceptor-example ---
[INFO] Deleting D:\example-projects\spring-mvc\async-handler-interceptor-example\target
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ async-handler-interceptor-example ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\async-handler-interceptor-example\src\main\resources
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ async-handler-interceptor-example ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 4 source files with javac [debug target 25] to target\classes
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ async-handler-interceptor-example ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\async-handler-interceptor-example\src\test\resources
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ async-handler-interceptor-example ---
[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-handler-interceptor-example ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[WARNING] file.encoding cannot be set as system property, use <argLine>-Dfile.encoding=...</argLine> instead
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.logicbig.example.ControllerTest
20:11:33.291296600 interceptor#preHandle called. Thread: main, DispatcherType: REQUEST
20:11:33.299311700 MyController [main] controller#handler called
20:11:33.300294400 MyController [main] controller#handler finished
20:11:33.304296200 interceptor#afterConcurrentHandlingStarted called. Thread: main, DispatcherType: REQUEST
20:11:33.304296200 MyController [mvc-async-1] controller-callable#async task started
20:11:33.605741500 MyController [mvc-async-1] controller-callable#async task finished
20:11:33.607669100 interceptor#preHandle called. Thread: main, DispatcherType: ASYNC
20:11:33.616656100 interceptor#postHandle called. Thread: main, DispatcherType: ASYNC
20:11:33.617749300 interceptor#afterCompletion called. Thread: main, DispatcherType: ASYNC
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.194 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.134 s
[INFO] Finished at: 2026-08-16T20:11:33+08:00
[INFO] ------------------------------------------------------------------------
INFO: Completed initialization in 1 ms

Note: When testing with MockMvc, the async-completion dispatch is only reported with DispatcherType.ASYNC starting from spring-test 4.2.3.RELEASE. Earlier versions of MockMvcRequestBuilders.asyncDispatch() did not call request.setDispatcherType(DispatcherType.ASYNC), so interceptors and filters would still see DispatcherType.REQUEST on that second dispatch — even though a real servlet container correctly reports ASYNC.

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 - AsyncHandlerInterceptor Example Select All Download
  • async-handler-interceptor-example
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • MyAsyncHandlerInterceptor.java
        • test
          • java
            • com
              • logicbig
                • example

    See Also

    Join