Java for Web学习笔记(四十):Filter(2)AsyncContext和Filter
2017-03-09 11:27
543 查看
什么是异步请求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[/code]
相关链接:
我的Professional Java for Web Applications相关文章
相关文章推荐
- Java for Web学习笔记(四一):Filter(3)用于Log
- Java for Web学习笔记(四二):Filter(4)用于压缩
- Java for Web学习笔记(六八):Service和Repository(3)异步Async和调度Schedule
- Java for Web学习笔记(十六):JSP(6)jspx
- Java for Web学习笔记(二四):EL(4)流(Stream)
- Java for Web学习笔记(二八):JSTL(4)Core Tag(下)
- Java for Web学习笔记(二七):JSTL(3)Core Tag(中)
- Java for Web学习笔记(五七):Spring框架简介(6)代码设置
- Java for Web学习笔记(十七):Session(1)Session的携带
- Java for Web学习笔记(五一):Log(3)代码中使用log4j2
- Java for Web学习笔记(四四):WebSocket(1)演化历程
- Java for Web学习笔记(四六):WebSocket(3)Java Server
- Java for Web学习笔记(六):Servlet(4)HttpServletResponse
- Java for Web学习笔记(二):Web Containers
- Java for Web学习笔记(五二):Spring框架简介(1)特点简述
- Java for Web学习笔记(十):Servlet(8)下发文件
- Java for Web学习笔记(二三):EL(3)EL的视图
- Java for Web学习笔记(二五):JSTL(1)使用JSTL
- Java for Web学习笔记(二十):Session(4)在集群中使用Session
- Java for Web学习笔记(四七):WebSocket(4)Java Client和二进制消息