Servlet JSP Tutorial -- 1.14 Async Servlet Example

Keywords: Java Attribute Apache

Asynchronous servlet is introduced in Servlet 3. This is a good way to deal with thread starvation when running threads for long periods of time.

Async Servlet

Before we understand what an asynchronous Servlet is, let's try to understand why we need it.

Suppose we have a Servlet that takes a lot of time, like the following.

package com.journaldev.servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/LongRunningServlet")
public class LongRunningServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        long startTime = System.currentTimeMillis();
        System.out.println("LongRunningServlet Start::Name="
                + Thread.currentThread().getName() + "::ID="
                + Thread.currentThread().getId());

        String time = request.getParameter("time");
        int secs = Integer.valueOf(time);
        // max 10 seconds
        if (secs > 10000)
            secs = 10000;

        longProcessing(secs);

        PrintWriter out = response.getWriter();
        long endTime = System.currentTimeMillis();
        out.write("Processing done for " + secs + " milliseconds!!");
        System.out.println("LongRunningServlet Start::Name="
                + Thread.currentThread().getName() + "::ID="
                + Thread.currentThread().getId() + "::Time Taken="
                + (endTime - startTime) + " ms.");
    }

    private void longProcessing(int secs) {
        // wait for given time before finishing
        try {
            Thread.sleep(secs);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

If we access a servlet with a URL of http://localhost:8080/Long Running Servlet?Time=8000 through a browser, we get a response of "Processing done for 8000 milliseconds!" in seconds. The heart is that if you look at the server date, you will find:

LongRunningServlet Start::Name=http-bio-8080-exec-34::ID=103
LongRunningServlet Start::Name=http-bio-8080-exec-34::ID=103::Time Taken=8002 ms.

So our servlet threads run for more than 8 seconds, although most processing is unrelated to servlet requests or responses.

This can lead to thread starvation - because our servlet threads are blocked until all processing is complete. If the server gets a lot of request processing, it will reach the maximum servlet thread limit, and further requests will get connection rejection errors.

The problem with container-specific solutions is that we cannot move to other servlet containers without changing the application code, which is why asynchronous servlet support has been added to S ervlet 3.0 to provide standard methods for asynchronous processing in servlets.

Asynchronous Servlet Implementation

Let's look at the steps to implement an asynchronous servlet, and then we'll give the asynchronous support servlet to the chestnut above.

  1. First, the servlet we want to provide asynchronous support should have the @WebServlet annotation with the asyncSupported value of true.
  2. Since the actual work is delegated to another thread, we should have a thread pool implementation. We can use the Executors framework to create thread pools and use the servlet context listener to start the thread pool.
  3. We need to get an instance of AsyncContext through the ServletRequest.startAsync() method. AsyncContext provides methods for obtaining ServletRequest and ServletResponse object references. It also provides a way to forward requests to another resource using the dispatch() method.
  4. We should have a Runnable implementation in which we perform heavy processing and then use the AsyncContext object to assign requests to another resource or use the ServletResponse object to write responses. Once the processing is complete, we should call the AsyncContext.complete() method to let the container know that the asynchronous processing is complete.
  5. We can add the AsyncListener implementation to the AsyncContext object to implement the callback method - we can use it to provide error response for errors or timeouts in asynchronous thread processing. We can also do some cleaning up here.

Once we complete our Async servlet sample project, it will be shown in the following figure.

Initializing Worker Thread Pool in Servlet Context Listener

package com.journaldev.servlet.async;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class AppContextListener implements ServletContextListener {

    public void contextInitialized(ServletContextEvent servletContextEvent) {

        // create the thread pool
        ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 200, 50000L,
                TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(100));
        servletContextEvent.getServletContext().setAttribute("executor",
                executor);

    }

    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        ThreadPoolExecutor executor = (ThreadPoolExecutor) servletContextEvent
                .getServletContext().getAttribute("executor");
        executor.shutdown();
    }

}

Implementation is very simple, if you are not familiar with Executors framework, please read Thread Pool Executor.

For more details about listeners, please go through Servlet Listener Example.

Worker Thread Implementation

package com.journaldev.servlet.async;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.AsyncContext;

public class AsyncRequestProcessor implements Runnable {

    private AsyncContext asyncContext;
    private int secs;

    public AsyncRequestProcessor() {
    }

    public AsyncRequestProcessor(AsyncContext asyncCtx, int secs) {
        this.asyncContext = asyncCtx;
        this.secs = secs;
    }

    @Override
    public void run() {
        System.out.println("Async Supported? "
                + asyncContext.getRequest().isAsyncSupported());
        longProcessing(secs);
        try {
            PrintWriter out = asyncContext.getResponse().getWriter();
            out.write("Processing done for " + secs + " milliseconds!!");
        } catch (IOException e) {
            e.printStackTrace();
        }
        //complete the processing
        asyncContext.complete();
    }

    private void longProcessing(int secs) {
        // wait for given time before finishing
        try {
            Thread.sleep(secs);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Notice the use of AsyncContext and its use in obtaining request and response objects, and then complete the asynchronous processing with the complete() method call.

AsyncListener Implementation

package com.journaldev.servlet.async;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebListener;

@WebListener
public class AppAsyncListener implements AsyncListener {

    @Override
    public void onComplete(AsyncEvent asyncEvent) throws IOException {
        System.out.println("AppAsyncListener onComplete");
        // we can do resource cleanup activity here
    }

    @Override
    public void onError(AsyncEvent asyncEvent) throws IOException {
        System.out.println("AppAsyncListener onError");
        //we can return error response to client
    }

    @Override
    public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
        System.out.println("AppAsyncListener onStartAsync");
        //we can log the event here
    }

    @Override
    public void onTimeout(AsyncEvent asyncEvent) throws IOException {
        System.out.println("AppAsyncListener onTimeout");
        //we can send appropriate response to client
        ServletResponse response = asyncEvent.getAsyncContext().getResponse();
        PrintWriter out = response.getWriter();
        out.write("TimeOut Error in Processing");
    }

}

Notice the implementation of the onTimeout() method that we send a timeout response to the client.

Async Servlet Example implementation

Here is the implementation of our asynchronous servlet. Note the use of AsyncContext and ThreadPool Executor for processing.

package com.journaldev.servlet.async;

import java.io.IOException;
import java.util.concurrent.ThreadPoolExecutor;

import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(urlPatterns = "/AsyncLongRunningServlet", asyncSupported = true)
public class AsyncLongRunningServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        long startTime = System.currentTimeMillis();
        System.out.println("AsyncLongRunningServlet Start::Name="
                + Thread.currentThread().getName() + "::ID="
                + Thread.currentThread().getId());

        request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);

        String time = request.getParameter("time");
        int secs = Integer.valueOf(time);
        // max 10 seconds
        if (secs > 10000)
            secs = 10000;

        AsyncContext asyncCtx = request.startAsync();
        asyncCtx.addListener(new AppAsyncListener());
        asyncCtx.setTimeout(9000);

        ThreadPoolExecutor executor = (ThreadPoolExecutor) request
                .getServletContext().getAttribute("executor");

        executor.execute(new AsyncRequestProcessor(asyncCtx, secs));
        long endTime = System.currentTimeMillis();
        System.out.println("AsyncLongRunningServlet End::Name="
                + Thread.currentThread().getName() + "::ID="
                + Thread.currentThread().getId() + "::Time Taken="
                + (endTime - startTime) + " ms.");
    }

}

Run Async Servlet web application

Now, when we run a servlet with a URL of http://localhost:8080/AsyncLong Running Servlet?Time=8000, we get the same response as the log:

AsyncLongRunningServlet Start::Name=http-bio-8080-exec-50::ID=124
AsyncLongRunningServlet End::Name=http-bio-8080-exec-50::ID=124::Time Taken=1 ms.
Async Supported? true
AppAsyncListener onComplete

If we run at 9999, a timeout will occur. The client response is "Time Out Error in Processing" and the response in the log:

AsyncLongRunningServlet Start::Name=http-bio-8080-exec-44::ID=117
AsyncLongRunningServlet End::Name=http-bio-8080-exec-44::ID=117::Time Taken=1 ms.
Async Supported? true
AppAsyncListener onTimeout
AppAsyncListener onError
AppAsyncListener onComplete
Exception in thread "pool-5-thread-6" java.lang.IllegalStateException: The request associated with the AsyncContext has already completed processing.
    at org.apache.catalina.core.AsyncContextImpl.check(AsyncContextImpl.java:439)
    at org.apache.catalina.core.AsyncContextImpl.getResponse(AsyncContextImpl.java:197)
    at com.journaldev.servlet.async.AsyncRequestProcessor.run(AsyncRequestProcessor.java:27)
    at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)
    at java.lang.Thread.run(Thread.java:680)

Note that the servlet thread completes its execution quickly, and all the major processing is being done in other threads.

That's all about Async Servlet, and I hope you like it.

Posted by heavenly on Thu, 13 Dec 2018 15:42:05 -0800