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.
- First, the servlet we want to provide asynchronous support should have the @WebServlet annotation with the asyncSupported value of true.
- 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.
- 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.
- 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.
- 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.