什么是异步请求AsyncContext
servlet2.5中,页面发送一次请求,是顺序执行,即使在servlet里的service中开启一个线程,线程处理后的结果是无法返回给页面的,因为servlet执行完毕后,response就关闭了,无法将后台更新数据即时更新到页面端。要实时推送,采用定时发送请求、Ajax 轮询、反向Ajax(Comnet)。在servlet3.0中提供了异步支持,当数据返回页面后,request并没有关闭,当服务器端有数据更新时,就可以推送了[1]
是否能不断地推送
这个和AsyncContent没有关系,而是和HttpServletResponse的PrintWriter有关,更重要的是和client(浏览器)的处理有关。看看下面的普通HttpServlet的小例子。我们期望在一定时间内,每隔1秒在页面上增加一行信息。
public class ServletOne extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
for(int i = 0 ; i < 5 ; i ++){
response.getWriter().print("--------- " + i + " ---------");
response.getWriter().flush(); //测试例子1,执行本句;测试例子2,注释掉本句
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}
我们先看看提供了flush()和无flush()两者的HTTP 200OK的消息包。
测试例子2中没有fulsh()的情况,Servlet等到最后,在构造的HTTP响应,因此可以或者具体消息体的长度。
测试例子1,由于通过flush()进行了强制输出,所有并不清楚最终的消息长度,所以消息头填入:Transfer-Encoding: "chunked"。我们通过抓包来看:
但是在客户端(火狐浏览器)中,我们并没有看到逐秒显示内容,而是5秒后,统一显示。如果我们将5秒的时间加大,改为一分钟,我们可以看到,大概在45秒左右,一次性显示之前的信息,然后开始每秒添加新的内容。因此,如何呈现由客户端决定,在无法明确用户使用何种客户端的情况下,不要对逐步呈现报有希望。同样的,通过异步线程输出的AsyncContext,在普通的HTML中,我们也不要对逐步呈现抱有期望。要解决,需要JavaScript,每个chuch是一个新的事件将触发JavaScript XMLHttpRequest对象的onreadystatechange事件处理。
Chunked Transfer Coding在HTTP/1.1标准的3.6.1中定义[2],抓包工具会这些TCP包合成为HTTP,我们看看结果。
无论是response.getOutputStream()还是response.getWriter()在flush()的时候,会自动补齐每个chucked数据结构,这些在仔细翻看tcp的抓包会找到,并在close()时给出最后一个chucked的标识。
AsyncContext小例子
//【1】要设置为支持asyncSupported,以便支持通过异步线程返回HTTP 200 OK
@WebServlet(asyncSupported = true, urlPatterns = { "/testAsync" })
public class TestAsyncServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//【2】建立AsyncContext,可以将request和response直接传递,或者重新wrapper后传递。
// final AsyncContext context = request.startAsync();
final AsyncContext context = request.startAsync(request,response);
// 【3】设置timeout,单位ms,如果在timeout时间之前,异步线程不能完成处理,则会抛出异常。
// 0表示没有timeout时间限制,但这样做是危险的,可能会导致线程永久挂起。
context.setTimeout(10_000);
// 【4】设置Listner跟踪状态,一般情况不需要,本例供学习用。
context.addListener(new AsyncListener() {
@Override
public void onTimeout(AsyncEvent event) throws IOException {
System.out.println("onTimeout...");
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
// 不会看到有相关的打印信息,根据注解
// Notifies this AsyncListener that a new asynchronous cycle is being initiated via
// a call to one of the ServletRequest.startAsync methods
// 这是在前面调用的,已经是历史了。
System.out.println("onStartAsync...");
}
@Override
public void onError(AsyncEvent event) throws IOException {
System.out.println("onError...");
}
@Override
public void onComplete(AsyncEvent event) throws IOException {
System.out.println("onComplete...");
}
});
//【5】启动异步线程进行处理。如果我们在原来的ServletContext中进行输出(如下注释行),也是会反映到页面上的
// response.getWriter.println("Hello,ServletConext!");
context.start(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000); //模拟某些处理的花费时间
// 5.1】获取ServletResponse,同样可以通过context.getRequest()获取ServletRequest,进而获取更多的信息。
context.getResponse().getWriter().write("Hello, AsyncContext!");
// 5.2】必须明确通知AsyncContext已经完成;
// 否者即使异步线程结束,AsyncContext也不知道SerlvetResponse的outputStream要close,这会导致挂起。
context.complete(); //log输出onComplete...
} catch(IllegalStateException e1){
// 5.3】超时,会触发IllegalStateException错误,对应地我们会看到onTimeout...然后onComplete...
System.out.println("Received IllegalStateException, maybe timeout");
}catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
再看一个小例子
上面的例子已经包括了AsyncContext的基本用法,但是书中的例子,有几个特别的语法也让我翻了好一阵子Internet,所以还是应该介绍一下。在上面例子的基础上,稍作修改
@WebServlet(asyncSupported = true, urlPatterns = { "/testAsync" })
public class TestAsyncServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
// 为每个异步线程给一个流水号
private static volatile int ID = 1;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 这里使用了final。在方法里面使用final,表示这个值只允许赋值一次,不允许再次变更
final int id;
synchronized(AsyncServlet.class){
id = ID ++;
}
final AsyncContext context = request.startAsync();
context.setTimeout(10_000);
/* 这里的写法有些奇特,称为Method References。
* 在称为Method References中System.out::println 相当于lambda表达式的x -> System.out.println(x)
* 我们知道start()的参数是Runnable。下面的这段代码
* context.start(new Runnable() {
* @Override
* public void run() {
* thread.doWork();
* }
* });
* 相当于lambda表达式的
* context.start(()->thread.doWork());
* 相当于Method Preferences中的
* context.start(thread::doWork());
*/
AsyncThread thread = new AsyncThread(id, context);
context.start(thread::doWork);
}
/* 内部类可以有静态类
* 内部静态类不能有指向外部类对象引用,即除了static的属性或者方法都不能使用外部类,即不能应用外部类对象的属性。
* 对于非静态类,必须能够引用外部类的属性,也就是为何一个外部类的静态方法中,是无法创建内部类对象的原因,需要AA aa = new A().new AA();
*/
private static class AsyncThread{
private final int id;
private final AsyncContext context;
public AsyncThread(int id, AsyncContext context) {
this.id = id;
this.context = context;
}
public void doWork(){
System.out.println("Asynchronous thread started. Request ID = " + this.id + ".");
try {
Thread.sleep(5_000L);
} catch (Exception e) {
}
//下面演示获取request的参数
HttpServletRequest request = (HttpServletRequest)this.context.getRequest();
System.out.println("Done sleeping. Request ID = " + this.id + ", URL = " + request.getRequestURL() + ".");
//重定向到某个jsp。dispatch():Dispatches the request and response objects of this AsyncContext to the given path.
//因为已经递交了,无需也不能进行context.complete(),否则会报错
this.context.dispatch("/WEB-INF/jsp/view/async.jsp");
}
}
}
ASYNC Filter
在web.xml中声明Filter
我们为三种不同的dispatcher定义不同的filter名字,虽然都指向同一个filter类。
<filter>
<filter-name>normalFilter</filter-name>
<filter-class>cn.wei.flowingflying.chapter09.AnyRequestFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>normalFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
<filter>
<filter-name>forwardFilter</filter-name>
<filter-class>cn.wei.flowingflying.chapter09.AnyRequestFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>forwardFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
<filter>
<filter-name>asyncFilter</filter-name>
<filter-class>cn.wei.flowingflying.chapter09.AnyRequestFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>asyncFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>
重新封装Request或者Response
ServletRequest和网络收到的HTTP数据包相关,内容是不能修改的,但如果我们希望对启动的某些参数进行格式修改,例如将param参数都该成大写,我们需要根据原来的ServletRequest重新封装(wrapper)。
public class MyRequestWrapper extends HttpServletRequestWrapper {
public MyRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
return StringUtils.upperCase(super.getParameter(name));
}
}
Filter代码
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Entering " + this.name + ".doFilter(). URL : " + ((HttpServletRequest)request).getRequestURL());
// 我们重新封装的request
chain.doFilter(new MyRequestWrapper((HttpServletRequest)request), response);
if(request.isAsyncSupported() && request.isAsyncStarted()){
AsyncContext context = request.getAsyncContext();
System.out.println("Leaving " + this.name + ".doFilter(), async " +
"context holds wrapped request/response = " + !context.hasOriginalRequestAndResponse());
}
}
Async Servlet的代码
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/*
* 我们比较一下request.startAsync()和request.startAsync(request, response),前者wrapper不起作用,后者起作用
* 在normalFilter filter中,重新封装了request。
* 在request.startAsync()中,将org.apache.catalina.connector.RequestFacade(原始的request)传递到AsyncContext
* 在request.startAsync(request,response)中,指定将filter中wrapper后的对象(MyRequestWrapper)传递到AsynContext中
*/
final AsyncContext context = request.getParameter("unwrap") != null ?
request.startAsync() : request.startAsync(request, response);
context.setTimeout(timeout);
AsyncThread thread = new AsyncThread(id, context);
context.start(thread::doWork);
}
何时触发ASYNC Filter
从log跟踪看,如果我们采用第一个Servlet小例子是不会触发ASYNC Filter的,在后面的servlet小例子中,通过dispatch进行了重导向,也就是从一个外部的URL,在异步中转到一个内部的URL,此时触发了filter。这很容易理解,在第一个Servlet中均在一个Servlet内部的处理,不可能触发Filter,只有通过重定向等方式,重新到达web container,才能触发顺序执行filter链。
在AsyncContext中不会触发到FORWARD Filter,而是触发ASYNC Filter。
this.context.dispatch("/WEB-INF/jsp/view/async.jsp");
Entering asyncFilter.doFilter(). URL : http://localhost:8080/chapter09/WEB-INF/jsp/view/async.jsp
相关链接: 我的Professional Java for Web Applications相关文章