Backend Development 22 min read

Asynchronous Processing in Servlet 3.0 and Spring MVC: Implementation Methods and Practical Applications

This article explains why and how to use asynchronous processing in Servlet 3.0 and Spring MVC, compares four implementation approaches (no return value, Callable, WebAsyncTask, DeferredResult), provides detailed code examples, and demonstrates a real‑world use case for decoupling long‑running tasks.

转转QA
转转QA
转转QA
Asynchronous Processing in Servlet 3.0 and Spring MVC: Implementation Methods and Practical Applications

1. Introduction

As an internet developer, most of my work involves designing and implementing APIs. Synchronous handling works for most cases, but when a time‑consuming request spikes in traffic, many web‑container threads become occupied, slowing down all responses. Asynchronous processing releases the container thread early, improving throughput for long‑running operations.

2. Native Servlet 3.0 Asynchronous Processing

In a synchronous model each request consumes a thread from the container pool until the response is sent, which can lead to thread starvation. With asynchronous processing the main thread starts async work, releases the thread back to the pool, and a separate thread completes the request.

Example code for a native async servlet:

@WebServlet(name = "AsyncServlet1", urlPatterns = "/asyncServlet1", asyncSupported = true)
public class AsyncLongRunningServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        long st = System.currentTimeMillis();
        logger.info("Main thread: " + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
        AsyncContext asyncContext = request.startAsync(request, response);
        asyncContext.start(() -> {
            long stSon = System.currentTimeMillis();
            System.out.println("Worker thread: " + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
            try {
                TimeUnit.SECONDS.sleep(10);
                asyncContext.getResponse().getWriter().write("ok");
                asyncContext.complete();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("Worker thread: " + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end, elapsed(ms):" + (System.currentTimeMillis() - stSon));
        });
        System.out.println("Main thread: " + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end, elapsed(ms):" + (System.currentTimeMillis() - st));
    }
}

The four steps for using servlet async are: enable asyncSupported, start async via request.startAsync , invoke AsyncContext.start to run the work, and finally call AsyncContext.complete() . Logs show the main thread returns quickly while the worker thread finishes the business logic.

3. Spring MVC Asynchronous Interface Implementations

Spring MVC wraps the Servlet 3.0 async support and determines async handling based on the controller method's return type.

3.1 Interface that Does Not Care About Return Value

When the result is irrelevant, a plain Java thread or the @Async annotation can be used.

Code example (no return value, manual thread):

@GetMapping("testAsync1")
public void testAsync1() {
    logger.info("Async request start");
    new Thread(this::asyncExecute1).start();
    logger.info("Async request end");
}
@SneakyThrows
public void asyncExecute1() {
    logger.info("Business start");
    TimeUnit.SECONDS.sleep(10);
    logger.info("Business end");
}

Using @Async (requires the async method to be in a different bean):

@GetMapping("testAsync2")
public void testAsync2() {
    StopWatch sw = new StopWatch("@Async test container thread");
    sw.start("container thread");
    logger.info("Async request start");
    testService.asyncExecute2();
    logger.info("Async request end");
    sw.stop();
    logger.info(String.format("%s seconds", sw.getTotalTimeSeconds()));
}
@Async
@SneakyThrows
public void asyncExecute2() {
    StopWatch sw = new StopWatch("@Async test worker thread");
    sw.start("worker thread");
    logger.info("Business start");
    TimeUnit.SECONDS.sleep(10);
    logger.info("Business end");
    sw.stop();
    logger.info(String.format("%s seconds", sw.getTotalTimeSeconds()));
}

3.2 Return Type Callable

When a result is needed, returning Callable<String> lets Spring execute the callable in a separate thread while the container thread is released.

@GetMapping("/testCallable")
@ResponseBody
public Callable
test2() {
    StopWatch sw = new StopWatch("Callable test container thread");
    sw.start("container thread");
    logger.info("Callable async request start");
    Callable
callable = () -> {
        StopWatch sw1 = new StopWatch("Callable test worker thread");
        sw1.start("worker thread");
        logger.info("Callable business start");
        Thread.sleep(10000);
        logger.info("Callable business end");
        sw1.stop();
        logger.info(String.format("%s seconds", sw1.getTotalTimeSeconds()));
        return "OK";
    };
    logger.info("Callable async request end");
    sw.stop();
    logger.info(String.format("%s seconds", sw.getTotalTimeSeconds()));
    return callable;
}

The response is sent only after the callable finishes, as shown by the logs and the final "OK" result.

3.3 Brief Overview of Asynchronous Processing Principle

Spring MVC first resolves the handler via DispatcherServlet#doDispatch , then invokes the handler adapter. The return value is inspected; if it is Callable , WebAsyncTask , or DeferredResult , Spring delegates to the corresponding HandlerMethodReturnValueHandler which starts async processing via WebAsyncManager . The manager submits the task to an executor, captures the result, and finally calls setConcurrentResultAndDispatch to resume the request processing.

3.4 Return Type WebAsyncTask

WebAsyncTask extends Callable and adds callbacks for timeout, error, and completion.

public void onTimeout(Callable
callback) { this.timeoutCallback = callback; }
public void onError(Callable
callback) { this.errorCallback = callback; }
public void onCompletion(Runnable callback) { this.completionCallback = callback; }

Note that onError handles system‑level exceptions, while business exceptions are processed by the completion callback.

3.5 Return Type DeferredResult

DeferredResult gives full control over when the async processing finishes. The controller creates a DeferredResult , returns it, and later code (or another request) calls setResult to complete the response.

public void startDeferredResultProcessing(final DeferredResult
deferredResult, Object... processingContext) throws Exception {
    try {
        interceptorChain.applyPreProcess(this.asyncWebRequest, deferredResult);
        deferredResult.setResultHandler(result -> {
            result = interceptorChain.applyPostProcess(this.asyncWebRequest, deferredResult, result);
            setConcurrentResultAndDispatch(result);
        });
    } catch (Throwable ex) {
        setConcurrentResultAndDispatch(ex);
    }
}

Typical usage example (create task, query status, and finish task):

private static final Map
> taskMap = new ConcurrentHashMap<>();
@RequestMapping("/createTask")
public DeferredResult
createTask(String uuid) {
    LOGGER.info("ID[{}] task start", uuid);
    StopWatch sw = new StopWatch("DeferredResult container thread");
    sw.start("container thread");
    LOGGER.info("DeferredResult async request start");
    DeferredResult
dr = new DeferredResult<>(100000L);
    StopWatch t = new StopWatch("DeferredResult worker thread");
    t.start("worker thread");
    dr.onCompletion(() -> {
        LOGGER.info("DeferredResult onCompletion work thread finished");
        t.stop();
        LOGGER.info(String.format("%s seconds", t.getTotalTimeSeconds()));
    });
    taskMap.put(uuid, dr);
    LOGGER.info("DeferredResult async request end");
    sw.stop();
    LOGGER.info(String.format("%s seconds", sw.getTotalTimeSeconds()));
    return dr;
}
@RequestMapping("/queryTaskState")
public String queryTaskState(String uuid) {
    DeferredResult
dr = taskMap.get(uuid);
    if (dr == null) return "Task not found, uid:" + uuid;
    return dr.hasResult() ? dr.getResult().toString() : "In progress";
}
@RequestMapping("/changeTaskState")
public String changeTaskState(String uuid) {
    DeferredResult
dr = taskMap.remove(uuid);
    if (dr == null) return "Task not found";
    if (dr.hasResult()) return "Already completed";
    dr.setResult("Completed");
    LOGGER.info("Set task ID {} to completed", uuid);
    return "Completed";
}

3.6 Summary of Spring MVC Async Return Types

Besides the three discussed types, Spring also supports ResponseBodyEmitter and other implementations of AsyncHandlerMethodReturnValueHandler .

4. Practical Application

DeferredResult is especially useful when a request depends on multiple downstream services. The article describes a workflow for creating a Git branch that involves TAPD, GitLab, Jenkins, and message queues. The controller returns a DeferredResult , releases the thread, and later a listener processes the result and calls setResult to finish the HTTP response.

Key code snippets illustrate storing DeferredResult in a map, sending a creation request to a MQ, and completing the result when the MQ consumer receives the final status.

5. Summary

All presented async patterns belong to reactive programming: they free the container thread, avoid blocking I/O, and enable parallel calls. Using these techniques can improve throughput under high load.

6. Postscript

Feel free to leave comments and share ideas.

backendasynchronousCallableSpring MVCDeferredResultWebAsyncTaskServlet 3.0
转转QA
Written by

转转QA

In the era of knowledge sharing, discover 转转QA from a new perspective.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.