原文地址:http://developer.51cto.com/art/201309/409211_all.htm
在Jave EE 6规范中,关于Servlet 3规范的相关增强功能,一直为大部分用户忽略,直到最新的Spring MVC 3.2才支持Servlet 3的异步调用。这可能跟大部分用户使用的JAVE EE容器依然是旧的有关系(如支持Servlet 3规范的需要Tomcat 7,但目前不少用户还在使用Tomcat 6)。
在本文中,将以实际的例子来讲解下Servlet 3规范中对异步操作的支持。
首先要简单了解,在Servlet 3中,已经支持使用注解的方式去进行Servlet的配置,这样就不需要在web.xml中进行传统的xml的配置了,最常用的注解是使用@WebServlet、@WebFilter、@WebInitParam,它们分别等价于传统xml配置中的、、init-param,其他参数可参考Servlet 3中的规范说明。
下面我们开始了解下,如果不使用异步特性的一个例子,代码如下:
- @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);
- //如果超过10秒,默认用10秒
- 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) {
- //故意让线程睡眠
- try {
- Thread.sleep(secs);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- }
运行上面的例子,输入
http://localhost:8080/AsyncServletExample/LongRunningServlet?time=8000,则可以看到输出为:
LongRunningServlet Start::Name=http-bio-8080-exec-34::ID=103
1. LongRunningServlet Start::Name=http-bio-8080-exec-34::ID=103::Time Taken=8002 ms.
可以观察到,在主线程启动后,servlet线程为了处理longProcessing的请求,足足等待了8秒,最后才输出结果进行响应,这样对于高并发的应用来说这是很大的瓶颈,因为必须要同步等到待处理的方法完成后,Servlet容器中的线程才能继续接收其他请求,在此之前,Servlet线程一直处于阻塞状态。
在Servlet 3.0规范前,是有一些相关的解决方案的,比如常见的就是使用一个单独的工作线程(worker thread)去处理这些耗费时间的工作,而Servlet 容器中的线程在把工作交给工作线程处理后则马上回收到Servlet容器中去。比如Tomcat的Comet、WebLogic的的FutureResponseServlet和WebSphere的Asynchronous Request Dispatcher都是这类型的解决方案。
但这些方案的弊端是没办法很容易地在不修改代码的情况下迁移到其他Servlet容器中,这就是Servlet 3中要定义异步Servlet的原因所在。
下面我们通过例子来说明异步Servlet的实现方法:
1、 首先设置servlet要支持异步属性,这个只需要设置asyncSupported属性为true就可以了。
2、 因为实际上的工作是委托给另外的线程的,我们应该实现一个线程池,这个可以通过使用Executors框架去实现,并且使用Servlet Context Listener去初始化线程池。
3、 我们需要通过ServletRequest.startAsync()方法获得AsyncContext的实例。AsyncContext提供了方法去获得ServletRequest和ServletResponse的对象引用。它也能使用dispatch()方法去将请求forward到其他资源。
4、 我们将实现Runnable接口,并且在其实现方法中处理各种耗时的任务,然后使用AsyncContext对象去将请求dispatch到其他资源中去或者使用ServletResponse对象输出。一旦处理完毕,将调用AsyncContext.complete()方法去让容器知道异步处理已经结束。
5、 我们还可以在AsyncContext对象增加AsyncListener的实现类以实现相关的回调方法,可以使用这个将错误信息返回给用户(如超时或其他出错信息),也可以做一些资源清理的工作。
我们来看下完成后例子的工程结构图如下:
下面我们看下实现了ServletContextListener类的监听类代码:
- AppContextListener.java
- 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) {
-
- // 创建线程池
- 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();
- }
-
- }
然后是worker线程的实现代码,如下:
- AsyncRequestProcessor.java
- 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();
- }
- //完成异步线程处理
- asyncContext.complete();
- }
-
- private void longProcessing(int secs) {
- // 休眠指定的时间
- try {
- Thread.sleep(secs);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
请在这里注意AsyncContext的使用方法,以及当完成异步调用时必须调用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");
- // 在这里可以做一些资源清理工作
- }
-
- @Override
- public void onError(AsyncEvent asyncEvent) throws IOException {
- System.out.println("AppAsyncListener onError");
- //这里可以抛出错误信息
- }
-
- @Override
- public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
- System.out.println("AppAsyncListener onStartAsync");
- //可以记录相关日志
- }
-
- @Override
- public void onTimeout(AsyncEvent asyncEvent) throws IOException {
- System.out.println("AppAsyncListener onTimeout");
- ServletResponse response = asyncEvent.getAsyncContext().getResponse();
- PrintWriter out = response.getWriter();
- out.write("TimeOut Error in Processing");
- }
-
- }
其中请注意可以监听onTimeout事件的使用,可以有效地返回给客户端出错的信息。最后来重新改写下前文提到的测试Servlet的代码如下:
- AsyncLongRunningServlet.java
- 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);
- // 如果超过10秒则设置为10秒
- 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.");
- }
-
- }
下面运行这个Servlet程序,输入:
http://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
但如果我们运行一个time=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)
可以看到,Servlet主线程很快执行完毕并且所有的处理额外的工作都是在另外一个线程中处理的,不存在阻塞问题。