Статьи

Асинхронный сервлет Особенность сервлета 3

Прежде чем мы начнем понимать, что такое Async Servlet, давайте попробуем понять, зачем нам это нужно. Допустим, у нас есть сервлет, который занимает много времени для обработки, что-то вроде ниже.

LongRunningServlet.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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-адресом http://localhost:8080/AsyncServletExample/LongRunningServlet?time=8000 , мы получим ответ «Обработка выполнена за 8000 миллисекунд !!» через 8 секунд. Теперь, если вы загляните в журналы сервера, вы получите следующий журнал:

1
2
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 + секунд, хотя большая часть обработки не имеет ничего общего с запросом или ответом сервлета.

Это может привести к истощению потока — поскольку наш поток сервлета блокируется до тех пор, пока не будет выполнена вся обработка, если сервер получит большое количество запросов на обработку, он достигнет максимального ограничения потока потока сервлета и последующие запросы получат ошибки Connection Refused.

До Servlet 3.0 существовало специальное решение для этих длинных потоков, в котором мы могли порождать отдельный рабочий поток для выполнения тяжелой задачи, а затем возвращать ответ клиенту. Поток сервлетов возвращается в пул сервлетов после запуска рабочего потока. Tomcat Comet, WebLogic FutureResponseServlet и WebSphere Asynchronous Request Dispatcher являются одними из примеров реализации асинхронной обработки.

Проблема с конкретным решением контейнера заключается в том, что мы не можем перейти к другому контейнеру сервлета без изменения кода нашего приложения, поэтому в Servlet 3.0 была добавлена ​​поддержка Async Servlet, чтобы обеспечить стандартный способ асинхронной обработки в сервлетах.

Реализация асинхронного сервлета

Давайте посмотрим шаги для реализации асинхронного сервлета, а затем мы предоставим асинхронный сервлет для примера выше.

  1. Прежде всего, сервлет, в котором мы хотим обеспечить асинхронную поддержку, должен иметь аннотацию @WebServlet со значением asyncSupported как true .
  2. Поскольку фактическая работа должна быть делегирована другому потоку, у нас должна быть реализация пула потоков. Мы можем создать пул потоков с помощью фреймворка Executors и использовать слушатель контекста сервлета для инициации пула потоков.
  3. Нам нужно получить экземпляр AsyncContext через ServletRequest.startAsync() . AsyncContext предоставляет методы для получения ссылок на объекты ServletRequest и ServletResponse. Он также предоставляет метод для пересылки запроса другому ресурсу с помощью метода dispatch ().
  4. У нас должна быть реализация Runnable, в которой мы будем выполнять тяжелую обработку и затем использовать объект AsyncContext для отправки запроса другому ресурсу или записи ответа с использованием объекта ServletResponse. После завершения обработки мы должны вызвать метод AsyncContext.complete (), чтобы контейнер знал, что асинхронная обработка завершена.
  5. Мы можем добавить реализацию AsyncListener к объекту AsyncContext для реализации методов обратного вызова — мы можем использовать это для предоставления ответа об ошибке клиенту в случае ошибки или тайм-аута при обработке асинхронного потока. Мы также можем сделать некоторые действия по уборке здесь.

Как только мы завершим наш проект для примера сервлета Async, он будет выглядеть как на рисунке ниже.

Асинхронный-сервлетов-пример

Инициализация пула рабочих потоков в прослушивателе контекста сервлета

AppContextListener.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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();
    }
 
}

Реализация довольно проста, если вы не знакомы с платформой Executors, пожалуйста, прочитайте Thread Pool Executor .

Для получения более подробной информации о слушателях, пожалуйста, прочитайте Учебник по сервлету .

Реализация рабочего потока

AsyncRequestProcessor.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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

AppAsyncListener.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 для обработки.

AsyncLongRunningServlet.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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-адресом как http://localhost:8080/AsyncServletExample/AsyncLongRunningServlet?time=8000 мы получим тот же ответ и логи, что и:

1
2
3
4
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, произойдет тайм-аут, и мы получим ответ на стороне клиента как «Ошибка тайм-аута при обработке» и в журналах:

01
02
03
04
05
06
07
08
09
10
11
12
13
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)

Обратите внимание, что поток сервлета быстро завершил выполнение, и вся основная обработка выполняется в другом потоке.

Вот и все для Async Servlet, надеюсь, вам понравилось.

Ссылка: Async Servlet Особенность Servlet 3 от нашего партнера JCG Панкаджа Кумара в блоге Developer Recipes .