11.3 编写异步Servlet

11.3 编写异步Servlet

写一个异步或异步Servlet过滤器比较简单。**当有一个需要相当长的时间完成的任务时,就需要为之创建一个异步的Servlet过滤器**。

编写异步Servlet或过滤器步骤

在异步Servlet过滤器类中需要做如下操作:
(1)调用ServletRequest中的startAsync方法。该startAsync返一个AsyncContext
(2)调用AsyncContextsetTimeout()方法,设置容器等待任务完成的超时时间的毫秒数。此步骤是可选的,但如果你不设置超时,容器的将使用默认的超时时间。如果任务未能在指定的超时时间内完成,将会抛出一个超时异常。
(3)调用asyncContext.start()方法,传递一个Runnable来执行一个长时间运行的任务。
(4)调用Runnableasynccontext.completeasynccontext.dispatch方法来完成任务。

异步Servlet的doGet或doPost模板

这里是一个异步ServletdoGetdoPost方法的框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//(1)获取AsyncContext对象
final AsyncContext asyncContext = servletRequest.startAsync();
//(2)设置超时时间
asyncContext.setTimeout( ... );
//(3)调用asyncContext.start方法,传入一个Runable来执行任务
asyncContext.start(new Runnable()
{
@Override
public void run() {
.....
// 上面是一个需要运行长时间的任务
// (4)任务完成,调用下面函数表示任务完成
asyncContext.complete() or asyncContext.dispatch()
}
})

实例1

一个简单的异步调度的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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package servlet;
import java.io.IOException;
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(
name = "AsyncDispatchServlet",urlPatterns =
{"/asyncDispatch"},asyncSupported = true
)
public class AsyncDispatchServlet extends HttpServlet
{
private static final long serialVersionUID = 222L;
@Override
public void doGet(final HttpServletRequest request,
HttpServletResponse response)
throws ServletException,IOException
{
// 1.获取AsyncContext实例
final AsyncContext asyncContext = request.startAsync();
// 保存主线程的名称
request.setAttribute("mainThread",
Thread.currentThread().getName());
// 2.设置超时时间为5秒钟
asyncContext.setTimeout(5000);
// 3.启动任务
asyncContext.start(new Runnable()
{
@Override
public void run()
{
// 一个长时间的任务
try {
Thread.sleep(3000);
} catch (InterruptedException e) {}
// 保存耗时任务线程的名称
request.setAttribute("workerThread",
Thread.currentThread().getName());
// 4.任务结束了,调用JSP页面展示
asyncContext.dispatch("/threadNames.jsp");
}
});
}
}

这个Servlet支持异步处理,其长期运行的任务就是简单地休眠5秒钟。为了证明这个耗时的任务是在不同的线程中执行的,而不是在主线程中执行的(即执行 ServletdoGet方法),它将主线程的名字和工作线程的ServletRequest分派到一个threadNames.jsp页面。该threadNames.jsp页面显示mainThreadWorkerThread变量。它们应打印不同的线程名字。

threadNames.jsp

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<title>Asynchronous servlet</title>
</head>
<body>
Main thread: ${mainThread}
<br/>
Worker thread: ${workerThread}
</body>
</html>

注意,你需要在任务结束后调用asyncContextdispatch方法或complete方法,所以它不会等待,直到它超时。
你可以把你的这个URL输入到浏览器来测试servlet
http://localhost:8080/app11a/asyncDispatch

运行效果

下图显示了主线程的名称和工作线程的名称。你在你的浏览器中看到的可能是不同的,但打印出的线程名字会有所不同,证明了工作线程与主线程不同。
这里有一张图片

除了调度到其他资源去完成任务,你也可以调用AsyncContextcomplete方法。此方法通知servlet容器该任务已完成。

实例2

发送最新进度更新的异步servle

Servlet每秒发送一次进度更新,使用户能够监测进展情况。它发送HTML响应和一个简单的JavaScript代码来更新HTML div元素。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package servlet;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class AsyncCompleteServlet extends HttpServlet
{
private static final long serialVersionUID = 78234L;
@Override
public void doGet(HttpServletRequest request,HttpServletResponse response)
throws ServletException,IOException
{
response.setContentType("text/html");
final PrintWriter writer = response.getWriter();
writer.println("<html><head><title>" + "Async Servlet</title></head>");
writer.println("<body><div id='progress'></div>");
// 1.获取AsyncContext实例
final AsyncContext asyncContext = request.startAsync();
// 2.设置超时时间
asyncContext.setTimeout(60000);
// 3.启动任务
asyncContext.start(new Runnable()
{
@Override
public void run()
{
System.out.println("new thread:"
+ Thread.currentThread());
// 向浏览器发送循环发送JS代码
for(int i = 0;i < 10;i++) {
writer.println("<script>");
writer.println("document.getElementById("
+ "'progress').innerHTML = '"
+ (i * 10) + "% complete'");
writer.println("</script>");
writer.flush();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
}
// 向浏览器发送最后一个JS
writer.println("<script>");
writer.println("document.getElementById("
+ "'progress').innerHTML = 'DONE'");
writer.println("</script>");
writer.println("</body></html>");
// 上面的是代码是一个耗时的任务
// 4.任务结束
asyncContext.complete();
}
});
}
}

以下这段代码负责发送进度更新:

1
2
3
4
5
6
writer.println("<script>");
writer.println("document.getElementById("
+ "'progress').innerHTML = '"
+ (i * 10) + "% complete'");
writer.println("</script>");
writer.flush();

浏览器将收到此字符串,其中x是10和100之间的数字。

1
2
3
<script>
document.getElementById('progress').innerHTML = 'x% complete'
</script>

因为该更新代码放在for循环中,将发送10次,浏览器也接收到10次javascript代码,进而执行javascript代码10次,也就是更新div标签10次从而呈现动态的效果.

部署描述符web.xml

为了向你展示如何通过在部署描述符中声明来编写一个Servlet异步,上面的AsyncCompleteServlet不用@WebServlet注解。而是使用下面的部署描述符(web.xml文件)来配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<servlet>
<servlet-name>AsyncComplete</servlet-name>
<servlet-class>servlet.AsyncCompleteServlet</servlet-class>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>AsyncComplete</servlet-name>
<url-pattern>/asyncComplete</url-pattern>
</servlet-mapping>
</web-app>

小结

在部署描述符中把servlet标签的async-supported子标签设置为true就可以支持异步的Servlet.

显示效果

你可以把你的URL输入到浏览器来测试AsyncCompleteServlet
http://localhost:8080/app11a/asyncComplete
部分显示效果如下:
……
……