2.12.3 Servlet3提供的异步处理

2.12.3 Servlet3提供的异步处理

在以前的Servlet规范中,如果Servlet作为控制器调用了一个耗时的业务方法,那么Servlet必须等到业务方法完全返回之后才会生成响应,这将使得Servlet对业务方法的调用变成一种阻塞式的调用,因此效率比较低。
Servlet3规范引入了异步处理来解决这个问题,异步处理允许Servlet重新发起一条新线程去调用耗时的业务方法,这样就可避免等待。

AsyncContext

Servlet3的异步处理是通过AsyncContext类来处理的,Servlet可通过ServletRequest的如下两个方开启异步调用、创建AsyncContext对象。

  • AsyncContext startAsync()
  • AsyncContext startAsync(ServletRequest, ServletResponse)

重复调用上面的方法将得到同一个AsyncContext对象。AsyncContext对象代表异步处理的上下文,它提供了一些工具方法,可完成设置异步调用的超时时长,dispatch用于请求、启动后台线程、获取requestresponse对象等功能

程序示例

项目结构

G:\Desktop\随书源码\轻量级Java EE企业应用实战(第5版)\codes\02\2.12\servlet3
├─async.jsp
├─upload.jsp
├─uploadFiles\
│ └─疯狂iOS讲义(上)—立体图.png
└─WEB-INF\
  ├─build.xml
  ├─classes\
  │ └─lee\
  │   ├─AsyncServlet.class
  │   ├─GetBooksTarget.class
  │   ├─MyAsyncListener.class
  │   └─UploadServlet.class
  ├─lib\
  │ ├─crazyit.jar
  │ ├─jstl.jar
  │ ├─leegang.jar
  │ └─standard.jar
  ├─src\
  │ └─lee\
  │   ├─AsyncServlet.java
  │   ├─GetBooksTarget.java
  │   ├─MyAsyncListener.java
  │   └─UploadServlet.java
  └─web.xml

AsyncServlet.java

下面是一个进行异步处理的Servlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package lee;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.*;
import java.util.*;

@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.setContentType("text/html;charset=GBK");
PrintWriter out = response.getWriter();
out.println("<title>异步调用示例</title>");
out.println("进入Servlet的时间:" + new java.util.Date() + ".<br/>");
// 创建AsyncContext,开始异步调用
AsyncContext actx = request.startAsync();
// 设置异步调用的超时时长
actx.setTimeout(60 * 1000);
// 启动异步调用的线程,该线程以异步方式执行
actx.start(new GetBooksTarget(actx));
out.println("结束Servlet的时间:" + new java.util.Date() + ".<br/>");
out.flush();
}
}

上面的Servlet类中代码:

1
2
3
4
5
6
// 创建AsyncContext,开始异步调用
AsyncContext actx = request.startAsync();
// 设置异步调用的超时时长
actx.setTimeout(60 * 1000);
// 启动异步调用的线程,该线程以异步方式执行
actx.start(new GetBooksTarget(actx));

创建了AsyncContext对象,并通过该对象以异步方式启动了一条后台线程。该线程执行体模拟调用耗时的业务方法.

GetBooksTarget.java

下面是线程执行体的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package lee;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.*;
import java.util.*;

public class GetBooksTarget implements Runnable {
private AsyncContext actx = null;

public GetBooksTarget(AsyncContext actx) {
this.actx = actx;
}

public void run() {
try {
// 等待5秒钟,以模拟业务方法的执行
Thread.sleep(5 * 1000);
ServletRequest request = actx.getRequest();
List<String> books = new ArrayList<String>();
books.add("疯狂Java讲义");
books.add("轻量级Java EE企业应用实战");
books.add("疯狂前端开发讲义");
request.setAttribute("books", books);
actx.dispatch("/async.jsp");
} catch (Exception e) {
e.printStackTrace();
}
}
}

该线程执行体内让线程暂停5秒来模拟调用耗时的业务方法,最后调用AsyncContextdispatch方法把请求dispatch到指定JSP页面.

async.jsp

被异步请求dispatch的目标页面需要指定session="false"“,表明该页面不会重新创建session。下面是async.jsp页面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page contentType="text/html; charset=GBK" language="java"
session="false"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<ul>
<c:forEach items="${books}" var="book">
<li>${book}</li>
</c:forEach>
</ul>
<%out.println("业务调用结束的时间:" + new java.util.Date());
if (request.isAsyncStarted()) {
// 完成异步调用
request.getAsyncContext().complete();
}%>

上面的页面只是一个普通JSP页面,只是使用了JSTL标签库来迭代输出books集合,因此读者需要将JSTL的两个JAR包复制到Web应用的WEB-INF\lib路径下。

为Servlet开启异步调用

对于希望启用异步调用的Servlet而言,开发者必须显式指定开启异步调用,为Servlet开启异步调用有两种方式。

  1. @WebServlet指定asyncSupported=true属性
  2. web.xml文件的<servlet>元素中增加<async-supported>子元素

web.xml

例如,希望开启上面Servlet的异步调用可通过如下配置片段:

1
2
3
4
5
6
7
8
9
<servlet> 
<servlet-name>async</servlet-name>
<servlet-class>lee.AsyncServlet</servlet-class>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>async</servlet-name>
<url-pattern>/async</url-pattern>
</servlet-mapping>

对于支持异步调用的Servlet来说,当Servlet以异步方式启用新线程之后,该Servlet的执行不会被阻塞,该Servlet将可以向客户端浏览器生成响应——当新线程执行完成后,新线程生成的响应再次被送往客户端浏览器。
通过浏览器访问上面的Servlet将看到如图2.51所示的页面

异步监听器

Servlet启用异步调用的线程之后,该线程的执行过程对开发者是透明的。但在有些情况下,开发者需要了解该异步线程的执行细节,并针对特定的执行结果进行针对性处理,这可借助于Servlet3提供的异步监听器来实现。
异步监听器需要实现AsyncListener接口,实现该接口的监听器类需要实现如下4个方法。

  • onStartAsync(AsyncEvent even):当异步调用开始时触发该方法。
  • onComplete(AsyncEvent even):当异步调用完成时触发该方法。
  • onError(AsyncEvent even):当异步调用出错时触发该方法。
  • onTimeout(AsyncEvent even):当异步调用超时时触发该方法。

MyAsyncListener.java

接下来为上面的异步调用定义如下监听器类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package lee;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;

public class MyAsyncListener implements AsyncListener {
public void onComplete(AsyncEvent event) throws IOException {
System.out.println("------异步调用完成------" + new Date());
}

public void onError(AsyncEvent event) throws IOException {
}

public void onStartAsync(AsyncEvent event) throws IOException {
System.out.println("------异步调用开始------" + new Date());
}

public void onTimeout(AsyncEvent event) throws IOException {
}
}

上面实现的异步监听器类只实现了onStartAsynconComplete两个方法,表明该监听器只能监听异步调用开始、异步调用完成两个事件。

给AsyncContext注册异步监听器

提供了异步监听器之后,还需要通过AsyncContext来注册监听器,调用该对象的addListener()方法即可注册监听器。例如,在上面的Servlet中增加如下代码即可注册监听器:

1
2
3
AsyncContext actx=request.startAsync();
//为该异步调用注册监听器
actx.addListener(new MyAsyncListener());

为异步调用注册了监听器之后,接下来的异步调用过程将会不断地触发该监听器的不同方法。
虽然上面的MyAsyncListener监听器类可以监听异步调用开始、异步调用完成两个事件,但从实际运行的结果来看,它并不能监听到异步调用开始事件,这可能是因为注册该监听器时异步调用已经开始了的缘故。
需要指出的是,虽然上面介绍的例子都是基于Servlet的,但由于FilterServlet具有很大的相似性,因此Servlet3规范完全支持在Filter中使用异步调用。在Filter中进行异步调用与在Servlet中进行异步调用的效果完全相似,故此处不再赘述。