Автор оригинала: Pankaj Kumar.
Асинхронный сервлет был представлен в сервлете 3. Это отличный способ справиться с проблемой нехватки потоков при длительных потоках.
Асинхронный Сервлет
Прежде чем мы перейдем к пониманию того, что такое асинхронный сервлет, давайте попробуем понять, зачем он нам нужен.
Допустим, у нас есть сервлет, обработка которого занимает много времени, как показано ниже.
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(); } } }
Если мы нажмем выше сервлета через браузер с URL как https://localhost:8080/AsyncServletExample/LongRunningServlet?time=8000
, мы получаем ответ “Обработка выполнена за 8000 миллисекунд!!” через 8 секунд. Теперь, если вы заглянете в журналы сервера, вы получите следующий журнал:
LongRunningServlet Start::Name=http-bio-8080-exec-34::ID=103 LongRunningServlet Start::Name=http-bio-8080-exec-34::ID=103::Time Taken=8002 ms.
Таким образом, наш поток сервлетов выполнялся в течение ~8+ секунд, хотя большая часть обработки не имеет ничего общего с запросом или ответом сервлета.
Это может привести к Голоданию потока – так как наш поток сервлета заблокирован до тех пор, пока не будет выполнена вся обработка. Если сервер получит много запросов для обработки, он достигнет максимального предела потока сервлета, и дальнейшие запросы будут получать ошибки отказа в подключении.
До Servlet 3.0 для этих длительных потоков существовало специальное контейнерное решение, в котором мы могли создавать отдельный рабочий поток для выполнения тяжелой задачи, а затем возвращать ответ клиенту. Поток сервлета возвращается в пул сервлетов после запуска рабочего потока. Комета Tomcat, Weblogic FutureResponseServlet и Диспетчер асинхронных запросов Websphere являются некоторыми примерами реализации асинхронной обработки.
Проблема с решением для конкретного контейнера заключается в том, что мы не можем перейти в другой контейнер сервлетов без изменения кода нашего приложения, поэтому в Servlet 3.0 была добавлена поддержка асинхронных сервлетов, чтобы обеспечить стандартный способ асинхронной обработки в сервлетах.
Асинхронная Реализация Сервлета
Давайте рассмотрим шаги по реализации асинхронного сервлета, а затем мы предоставим асинхронный поддерживаемый сервлет для приведенного выше примера.
- Прежде всего, сервлет, в котором мы хотим обеспечить поддержку асинхронности, должен иметь @WebServlet | аннотацию с поддержкой ASYNC значением true . Поскольку фактическая работа должна быть делегирована другому потоку, у нас должна быть реализация пула потоков. Мы можем создать
- пул потоков с помощью платформы исполнителей и использовать servletcontextlistener для инициализации пула потоков. Нам нужно получить экземпляр
- AsyncContext через ServletRequest.startAsync()
метод. AsyncContext предоставляет методы для получения ссылок на объекты ServletRequest и ServletResponse. Он также предоставляет метод для пересылки запроса на другой ресурс с использованием метода dispatch ().
У нас должна быть - Запускаемая реализация , в которой мы будем выполнять тяжелую обработку, а затем использовать объект AsyncContext либо для отправки запроса на другой ресурс, либо для записи ответа с использованием объекта ServletResponse. Как только обработка будет завершена, мы должны вызвать метод AsyncContext.complete (), чтобы сообщить контейнеру, что асинхронная обработка завершена. Мы можем добавить реализацию AsyncListener в объект AsyncContext для реализации методов обратного вызова – мы можем использовать это для предоставления ответа на ошибку клиенту в случае ошибки или тайм-аута во время обработки асинхронного потока. Мы также можем провести здесь кое-какие работы по очистке.
Как только мы завершим наш проект для примера асинхронного сервлета, он будет выглядеть следующим образом.
Инициализация пула рабочих потоков в Прослушивателе контекста Сервлета
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(100)); servletContextEvent.getServletContext().setAttribute("executor", executor); } public void contextDestroyed(ServletContextEvent servletContextEvent) { ThreadPoolExecutor executor = (ThreadPoolExecutor) servletContextEvent .getServletContext().getAttribute("executor"); executor.shutdown(); } }
Реализация довольно проста, если вы не знакомы с фреймворком исполнителей, пожалуйста, прочтите раздел Исполнитель пула потоков .
Для получения более подробной информации о прослушивателях, пожалуйста, ознакомьтесь с примером прослушивателя сервлета .
Реализация Рабочего потока
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(); } } }
Обратите внимание на использование AsyncContext
и его использование для получения объектов запроса и ответа, а затем завершения асинхронной обработки с помощью вызова метода complete ().
Реализация AsyncListener
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"); } }
Обратите внимание на реализацию метода onTimeout ()
, в котором мы отправляем клиенту ответ на тайм-аут.
Пример реализации асинхронного Сервлета
Вот реализация нашего асинхронного сервлета, обратите внимание на использование AsyncContext и ThreadPoolExecutor для обработки.
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."); } }
Запуск асинхронного веб-приложения Сервлета
Теперь, когда мы запустим вышеуказанный сервлет с URL-адресом как https://localhost:8080/AsyncServletExample/AsyncLongRunningServlet?time=8000
мы получаем тот же ответ и журналы, что и:
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
Если мы запускаемся со временем 9999, наступает тайм-аут, и мы получаем ответ на стороне клиента как “Ошибка тайм-аута при обработке” и в журналах:
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)
Обратите внимание, что поток сервлета быстро завершил выполнение, и вся основная работа по обработке выполняется в другом потоке.
Это все для асинхронного сервлета, надеюсь, вам понравилось.