Understanding Servlet 3.0 Asynchronous Processing in Spring MVC and Spring Boot
This article explains the fundamentals of Servlet 3.0 asynchronous processing, demonstrates how Spring MVC builds on the Servlet API, and provides multiple practical examples—including starting async support, using AsyncContext, custom threads, listeners, timeouts, and real‑world scenarios—to help developers master non‑blocking request handling in Java web applications.
Many readers ask why Spring Boot tutorials start with Servlets. Although direct Servlet usage is rare nowadays, Spring MVC is built on top of the Servlet API, so mastering Servlet fundamentals is essential for understanding Spring's async capabilities.
1. Early Servlet Request Flow
Before Servlet 3.0, a request is processed entirely by a thread taken from the container's main thread pool, which handles business logic and sends the response before returning the thread. This model can exhaust the thread pool when long‑running tasks block the main thread.
2. Servlet 3.0 Asynchronous Flow
Servlet 3.0 allows the original thread to start async processing, hand the request over to another thread, and then return to the pool. The async thread completes the response later, freeing the main thread for new requests.
3. Steps to Use Async Processing in Servlet 3.0
Step 1: Enable Async Support
@WebServlet(asyncSupported = true)Step 2: Start Async Request
AsyncContext asyncContext = request.startAsync(request, response);Step 3: Process Business Logic and Complete
Two common ways:
Start a new thread manually and call asyncContext.complete() after writing the response.
Use asyncContext.start(Runnable) which runs the Runnable in a container‑managed thread.
4. Example 1 – Using asyncContext.start
The servlet logs timestamps from the main and child threads, sleeps for 2 seconds to simulate a long task, writes "ok" to the response, and then calls asyncContext.complete(). The main thread finishes instantly, while the child thread finishes after the simulated delay.
package com.javacode2018.springboot.lesson002.demo1;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@WebServlet(name = "AsyncServlet1", urlPatterns = "/asyncServlet1", asyncSupported = true)
public class AsyncServlet1 extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long st = System.currentTimeMillis();
System.out.println("Main thread: " + Thread.currentThread() + "-" + st + "-start");
AsyncContext asyncContext = request.startAsync(request, response);
asyncContext.start(() -> {
long stSon = System.currentTimeMillis();
System.out.println("Child thread: " + Thread.currentThread() + "-" + stSon + "-start");
try {
TimeUnit.SECONDS.sleep(2);
asyncContext.getResponse().getWriter().write("ok");
asyncContext.complete();
} catch (Exception e) { e.printStackTrace(); }
System.out.println("Child thread: " + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end, cost(ms):" + (System.currentTimeMillis() - stSon));
});
System.out.println("Main thread: " + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end, cost(ms):" + (System.currentTimeMillis() - st));
}
}5. Example 2 – Custom Thread
Instead of asyncContext.start, a developer can create a custom Thread (or thread pool) and store the AsyncContext reference, then write the response and call complete() from that thread.
Thread thread = new Thread(() -> {
long stSon = System.currentTimeMillis();
System.out.println("Child thread: " + Thread.currentThread() + "-" + stSon + "-start");
try { TimeUnit.SECONDS.sleep(2); asyncContext.getResponse().getWriter().write(System.currentTimeMillis() + ",ok"); asyncContext.complete(); }
catch (Exception e) { e.printStackTrace(); }
System.out.println("Child thread: " + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end, cost(ms):" + (System.currentTimeMillis() - stSon));
});
thread.setName("CustomThread");
thread.start();6. Example 3 – Using asyncContext.dispatch()
After the async work finishes, the result can be stored in the request and asyncContext.dispatch() can forward the request back to the same servlet. The servlet checks request.getDispatcherType() to differentiate between the original request and the async forward.
if (request.getDispatcherType() == DispatcherType.ASYNC) {
Object result = request.getAttribute("result");
response.getWriter().write(result.toString());
} else {
// start async, process, set attribute, then asyncContext.dispatch();
}7. Example 4 – Setting a Timeout
Async requests can be given a timeout with asyncContext.setTimeout(milliseconds). If the timeout expires before complete() or dispatch(), the container triggers the onTimeout listener.
8. Example 5 – Adding Listeners
Listeners can react to lifecycle events: onComplete, onTimeout, onError, and onStartAsync. Each method can write a small marker to the response for demonstration.
asyncContext.addListener(new AsyncListener() {
@Override public void onComplete(AsyncEvent e) throws IOException { e.getAsyncContext().getResponse().getWriter().write("
onComplete"); }
@Override public void onTimeout(AsyncEvent e) throws IOException { e.getAsyncContext().getResponse().getWriter().write("
onTimeout"); }
@Override public void onError(AsyncEvent e) throws IOException { e.getAsyncContext().getResponse().getWriter().write("
onError"); }
@Override public void onStartAsync(AsyncEvent e) throws IOException { e.getAsyncContext().getResponse().getWriter().write("
onStartAsync"); }
});9. Example 6 – Coordinating Completion, Timeout, and Errors
An AtomicBoolean finish is used to guarantee that only one of the three possible paths (normal completion, timeout, error) writes the result and triggers a dispatch. This prevents the "AsyncContext already completed" exception shown in earlier examples.
10. Example 7 – Simulating a Business Scenario with MQ
Service A receives a request, stores the AsyncContext in a concurrent map, and returns immediately. Service B processes the request asynchronously (e.g., via a message queue) and later notifies Service A by calling the stored AsyncContext with the result, completing the original client request.
11. Summary
Start async processing with request.startAsync to obtain an AsyncContext.
Set a timeout using asyncContext.setTimeout.
Add listeners via asyncContext.addListener to handle completion, timeout, errors, and start events.
Finish async work either by asyncContext.dispatch() (forwarding) or asyncContext.complete() (direct response).
12. Source Code Repository
All demo code is available at https://gitee.com/javacode2018/springboot-series .
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Full-Stack Internet Architecture
Introducing full-stack Internet architecture technologies centered on Java
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
