Java for Web学习笔记(四十):Filter(2)AsyncContext和Filter

什么是异步请求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相关文章
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值