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的基本用法,但是书中的例子,有几个特别的语法也让我翻了好一阵子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"); } } }我们为三种不同的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>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)); } }从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相关文章