(第五章)Servlet进阶API、过滤器与监听器

第五章Servlet进阶API、过滤器与监听器

目录

第五章Servlet进阶API、过滤器与监听器

1、Servlet进阶API

1.1Servlet、ServletConfig与GenericServlet

1.2使用ServletConfig

1.3使用ServletContext

2.应用程序事件、监听器

2.1ServletContext事件、监听器

2.2HttpSession事件、监听器

2.3HttpServletRequest事件、监听器

3、过滤器

3.1过滤器的概念

3.2实现与设置过滤器

3.3请求封装器

3.4响应封装器

4、异步处理

4.1AsyncContext简介

4.2模拟服务器推播

4.3更多AsyncContext细节


1、Servlet进阶API

每个Servlet都必须由Web容器去读Servlet设置(无论使用标注还是web.xml)、初始化等,才可以真正成为一个Servlet。对于每个Servlet的设置信息,web容器会为其生成一个ServletConfig作为代表对象,你可以从该对象取得Servlet初始化参数,以及代表整个web应用程序的ServletContext对象。

1.1Servlet、ServletConfig与GenericServlet

在Servlet接口上,定义了与Servlet生命周期及请求服务相关的init()、service()与destroy()三个方法。每一次请求来到容器时,会产生HttpServletRequest与HttpServletResponse对象,并在调用service()方法时当作参数传入。

在Web容器启动后,会读取Servlet设置信息,将Servlet类加载并实例化,并未每个Servlet设置信息产生一个ServletConfig对象,而后调用Servlet接口的init()方法,并将产生的ServletConfig对象当作参数传入。

ServletConfig即每个Servlet设置的代表对象,容器会为每个Servlet设置信息产生一个Servlet及ServletConfig实例。GenericServlet同时实现了Servlet及ServletConfig。

GenericServlet主要的目的,就是将初始Servlet调用init()方法传入的ServletConfig封装起来:

1.2使用ServletConfig

ServletConfig相当于个别Servlet的设置信息代表对象,这意味着可以从ServletConfig中取得Servlet设置信息。ServletConfig定义了getInitParameter()、getInitParameterNames()方法,可以取得设置Servlet时的初始参数。

若要使用标注设置个别Servlet的初始参数,可以在@WebServlet中使用@WebInitParam设置initParam属性。例如:

若要在web.xml中设置个别Servlet的初始参数,可以在<servlet>标签中使用<init-param>等标签进行设置,web.xml中的设置会覆盖标注中的设置。例如:

由于ServletConfig必须在Web容器将Servlet实例化后,调用有参数的init()方法再将之传入,是与Web应用程序资源相关的对象,所以在继承HttpServlet后,通常会重新定义无参数的init()方法以进行Servlet初始参数的取得。GenericServlet定义了一些方法,将ServletConfig封装起来,便于取得设置信息,所以取得Servlet初始参数的代码也可以改写为:

1.3使用ServletContext

ServletContext接口定义了运行Servlet的应用程序环境的一些行为和观点,可以使用ServletContext实现对象来取得所请求资源的URL、设置与储存属性、应用程序初始参数,甚至动态设置Servlet实例。

当整个Web应用程序加载Web容器之后,容器会生成一个ServletContext对象作为整体应用程序的代表,并设置给ServletConfig,只要通过ServletConfig的getServletContext()方法就可以取得ServletContext对象。

1.getRequestDispatcher()

用来取得RequestDispatcher实例,使用时路径的指定必须以“/”作为开头,这个斜杠代表应用程序环境根目录。取得RequestDispatcher实例之后,就可以进行请求的转发(Forward)和包含(Include)。

2.getResourcePaths()

如果想要知道Web应用程序的某个目录中有哪些文件,则可以使用getResource()方法。例如:

for(String avatar : getServletContext().getResourcePaths("/")){
    //显示avatar文字...
}

3.getResourceAsStream()

如果想在Web应用程序中读取某个文件的内容,则可以使用getResourceAsStream()方法,使用时指定路径必须以“/”作为开头,表示相对于应用程序环境根目录,或者相对是/WEB-INF/lib中JAR文件里META-INF/resource的路径,运行结果会返回InputStream实例,接着就可以运行它来读取文件内容。

2.应用程序事件、监听器

Web容器管理Servlet/JSP相关的对象生命周期,若对HttpServletRequest对象、HttpSession对象、ServletContext对象在生成、销毁或相关属性设置发送的时机点有兴趣,则可以实现对应的监听器(Listener),做好相关的设置,这样在对应的时机点发生时,Web容器就会调用监听器上相对应的方法,让你在对应的时机点做些处理。

2.1ServletContext事件、监听器

与ServletContext相关的监听器有ServletContextListener与ServletContextAttributeListener。

1.ServletContextListener

ServletContextListener是“生命周期监听器”,如果想要知道何时Web应用程序已经初始化或即将结束销毁,可以实现ServletContextListener:

package javax.servlet;
import java.util.EventListener;
public interface ServletContextListener extends EventListener{
    public void contextInitialized(ServletContextEvent sce);
    public void contextDestroyed(ServletContextEvent sce);
}

在Web应用程序初始化后或即将结束销毁前,会调用ServletContextListener实现类相对应的contextInitialized()或contextDestroyed()。可以在contextInitialized()中实现应用程序资源的准备动作,在contextDestroyed()实现释放应用程序资源的动作。

例如,可以实现ServletContextListener,在应用程序初始过程中,准备好数据库连线对象、读取应用程序设置等动作,如放置使用头像的目录信息就不宜将目录名称写死。以免日后目录变动名称或位置时,所有相关的Servlet都需要进行源代码的修改。

ServletContextListener可以直接使用@WebListener标注,而且必须实现ServletContextListener接口,这样容器就会在启动时加载并运行对应的方法。当Web容器调用contextInitialized()或contextDestroyed()时,会传入ServletContext-Event,其封装了ServletContext,通过ServletContext的getInitParamter()方法来读取初始参数,因此Web应用程序初始参数常被称为ServletContext初始参数。

在整个Web应用程序生命周期,Servlet需共享的资料可以设置为ServletContext属性。由于ServletContext在Web应用程序存货期间都会一直存在,所有设置为ServletContext属性的数据,除非主动移除,否则也是一直存活于Web应用程序中。

因为@WebListener没有设置初始参数的属性,所有仅适用于无须设置初始参数的情况。如果需要设置初始参数,可以在web.xml中设置。

在web.xml中,使用<context-param>标签来定义初始参数。使用<listener>与<listener-class>标签来定义实现了Servlet-ContextListener接口的类名称。

2.ServletContextAttributeListener

ServletContextAttributeListener是“监听属性改变的监听器”,如果想要对象被设置、移除或替换ServletContext属性,可以收到通知以进行一些操作,则可以实现ServletContextAttributeListener。

package javax.servlet;
import java.util.EventListener;
public interface ServletContextAttributeListener extends EventListener{
    public void attributeAdded(ServletContextAttributeEvent scab);
    public void attributeRemoved(ServletContextAttributeEvent scab);
    public void attributeReplaced(ServletContextAttributeEvent scab);
}

当在ServletContext中添加属性、移除属性或替换属性时,相对应的attributeAdded()、attributeRemoved()与attributeReplaced()方法就会被调用。

2.2HttpSession事件、监听器

与HttpSession相关的监听器有四个:HttpSessionListener、HttpSessionAttributeListener、HttpSessionBindingListener与HttpSessionActivationListener。

1.HttpSessionListener

HttpSessionListener是“生命周期监听器”,如果想要在HttpSession对象创建或结束时,做些相对动作,则可以实现HttpSessionListener。

package javax.servlet.http;
import java.util.EventListener;
public interface HttpSessionListener extends EventListener{
    public void sessionCreated(HttpSessionEvent se);
    public void sessionDestroyed(HttpSessionEvent se);
}

在HttpSession对象初始化或结束前,会分别调用sessionCreated()与sessionDestroyed()方法,使用getSession()取得HttpSession,以针对会话对象作出相对应的创建或结束处理操作。

例子:

有些网站为了防止用户重复登录,会在数据库中以某个字段代表用户是否登录,用户登录后,在数据库中设置该字段信息,代表用户已登录,而用户注销后,再重置该字段。如果用户已登录,在注销前尝试再用另一个浏览器进行登录,应用程序会检查数据库中代表登录与否的字段,如果发现已被设置为登录,则拒绝用户重复登录。

现在的问题在于,如果用户在注销前不小心关闭浏览器,没有确实运行注销操作,那么数据库中代表登录与否的字段就不会被重置。为此,可以实现HttpSessionListener,由于HttpSession有其存活期限,当容器销毁某个HttpSession时,就会调用sessionDestroyed(),就可以在当中判断要重置哪个用户数据库代表登录与否的字段。

2.HttpSessionAttributeListener

HttpSessionAttributeListener是“属性改变监听器”,当在会话对象中加入属性、移除属性或替换属性时,相对应的attributeAdded()、attributeRemoved()与attributeReplaced()方法就会被调用,并分别传入HttpSessionBindingEvent。

package javax.servlet.http;
import java.util.EventListener;
public interface HttpSessionAttributeListener extends EventListener{
    public void attributeAdded(HttpSessionBindingEvent se);
    public void attributeRemoved(HttpSessionBindingEvent se);
    public void attributeReplaced(HttpSessionBindingEvent se);
}

HttpSessionBindingEvent有个getName()方法,可以取得属性设置或移除时指定的名称,而getValue()可以取得属性设置或移除时的对象。

3.HttpSessionBindingListener

HttpSessionBindingListener是“对象绑定监听器”,如果有个即将加入HttpSession的属性对象,希望在设置给HttpSession成为属性或从HttpSession中移除时,可以收到HttpSession的通知,则可以让该对象实现HttpSessionBindingListener接口。

package javax.servlet.http;
import java.util.EventListener;
public interface HttpSessionBindingListener extends EventListener{
    public void valueBound(HttpSessionBindingEvent event);
    public void valueUnbound(HttpSessionBindingEvent event);
}

4.HttpSessionActivationListener

HttpSessionActivationListener是“对象迁移监听器”,其定义了两个方法sessionWillPassivate()与sessionDidActivate()。很多情况下,几乎不会使用到HttpSessionActivationListener。在使用到分布式环境时。应用程序的对象可能分散在多个JVM中。当HttpSession要从一个JVM迁移到另一个JVM时,必须现在原本的JVM上序列化(Serialize)所有的属性对象,在这之前若属性对象有实现HttpSession-ActivationListener,就会调用sessionWillPassivate()方法,而HttpSession迁移至另一个JVM后,就会对所有属性对象作反序列化,此时会调用sessionDidActivate()方法。

2.3HttpServletRequest事件、监听器

与请求相关的监听器有3个:ServletRequestListener、ServletRequestAttributeListener与AsyncListener。

1.ServletRequestListener

ServletRequestListener是“生命周期监听器”,如果想在HttpServletRequest对象生成或结束时做些相对应的操作,则可以实现ServletRequestListener。

package javax.servlet;
import java.util.EventListener;
public interface ServletRequestListener extends EventListener{
    public void requestDestroyed(ServletRequestEvent sre);
    public void requestInitialized(ServletRequestEvent sre);
}

在ServletRequest对象初始化或结束前,会调用requestInitialized()与requestDestroyed()方法,可以通过传入的ServletRequestEvent来取得ServletRequest,以针对请求对象做出相对应的初始化或结束处理动作。

2.ServletRequestAttributeListener

ServletRequestAttriuteListener是“属性改变监听器”,在请求对象中加入属性、移除属性或替换属性时,相对应的attributeAdded()、attributeRemoved()与attributeReplaced()方法就会被调用,并分别传入ServletRequestAttributeEvent

ServletRequestAttributeEvent有个getName()方法,可以取得属性设置或移除时指定的名称,而getValue()则可以取得属性设置或移除时的对象。

3、过滤器

3.1过滤器的概念

想象已经开发好应用程序的主要商务功能了,但现在有几个需求出现:

(1)针对所有的Servlet,产品经理想要了解从请求到响应之间的时间差。

(2)针对某些特定的页面,客户希望只有特定几个用户才可以浏览。

(3)基于安全方面的考量,用户输入的特定字符必须过滤并替换为无害的字符。

(4)请求与响应的编码从Big5改用UTF-8。

以第一个需求而言,也许你的直觉就是,打开每个Servlet,在doXXX()开头与结尾取得系统时间,计算时间差,但如果页面有上百个或上千个,怎么完成这些需求?如果产品经理在你完成需求后,又要求拿掉计算时间差的功能,你怎么办?

收到这些需求的你,在急忙打开相关源代码文档进行修改之前,请先分析一下这些需求:

(1)运行Servlet的service()方法“前”,记录起始时间,Servlet的service()方法运行“后”,记录结束时间并计算时间差。

(2)运行Servlet的service()方法“前”,验证是否为允许的用户。

(3)运行Servlet的service()方法“前”,对请求参数进行字符过滤与替换。

(4)运行Servlet的service()方法“前”,对请求与响应对象设置编码。

经过以上分析,可以发现这些需求,可以在真正运行Servlet的service()方法“前”与Servlet的service()方法运行“后”中间进行实现。

性能测评、用户验证、字符替换、编码设置等需求,基本上与应用程序的业务需求没有直接的需求关系,只是应用程序额外的元件服务之一。你可能只是短暂需要它,或者需要整个系统应用相同设置,不应该为了一时的需要而修改代码强加入原有业务流程中。例如,性能的评测也许只是开发阶段才需要的,上线之后就要拿掉性能评测的功能,如果直接将性能评测的代码编写在业务流程中,那么拿掉这个功能,就又得再修改一次源代码。

因此,如性能测评、用户验证、字符替换、编码设置这类的需求,应该设计为独立的元件,随时可以加入应用程序中,也随时可以移除,或随时可以修改设置而不用修改原有的程序。这类元件就像是一个过滤器,安插在浏览器与Servlet之间,可以过滤请求与响应而作进一步处理。

Servlet/JSP提供了过滤器机制让你实现这些元件服务,可以视需求抽换过滤器或调整过滤器的顺序,也可以针对不同的URL应用不同的过滤器。甚至在不同的Servlet之间请求转发或包含时应用过滤器。

3.2实现与设置过滤器

在Servlet/JSP中要实现过滤器,必须实现Filter接口,并使用@WebFilter标注或在web.xml中定义过滤器,让容器知道该加载哪些过滤器类。Filter接口有三个要实现的方法:init()、doFilter()与destroy()。

package javax.servlet;
import java.io.IOException;
public interface Filter{
    public void init(FilterConfig filterConfig) throws ServletException;
    public void doFilter(ServletRequest request,Servlet response,
                        FilterChain chain) throws IOException,ServletException;
    public void destroy();
}

FilterConfig类似于Servlet接口init()方法参数上的ServletConfig,FilterConfig是实现Filter接口的类上使用标注或web.xml设置信息的代表对象。如果在定义过滤器时设置了初始参数,则可以通过FilterConfig的getInitParameter()方法来取得初始参数。

Filter接口的doFilter()方法类似于Servlet接口的service()方法。当请求来到容器,而容器发现调用Servlet的service()方法前,可以应用某过滤器时,就会调用该过滤器的doFilter()方法。可以在doFilter()方法中进行service()方法的前置处理,而后决定调用是否调用FilterChaindoFilter()方法。

如果调用了FilterChain的doFilter()方法,就会运行下一个过滤器,如果没有下一个过滤器了,就调用请求目标Servlet的service()方法。如果因为某个情况(如用户没有通过验证)而没有调用FilterChain的doFilter(),则请求就不会继续交给接下来的过滤器或目标Servlet。这就是所谓的拦截请求(从Servlet的观点看,根本不知道浏览器有发出请求)。FilterChain的doFilter()实现,概念是类似如下:

Filter filter = filterIterator.next();
if(filter != null){
    filter.doFilter(request,response,this);
}else{
    targetServlet.service(request,response);
}

在陆续调用完Filter实例的doFilter()仍至Servlet的service()之后,流程会以堆栈顺序返回,所以在FilterChain的doFilter()运行完毕后,就可以针对service()方法做后续处理。

只需要知道FilterChain运行后会以堆栈顺序返回即可。在实现Filter接口时,不用理会这个Filter前后是否有其他Filter,应该将之作为一个独立的元件设计。

如果调用Filter的doFilter()期间,因故抛出UnavailableException,此时不会继续下一个Filter,容器可以检验异常的isPermanet(),如果不是true,则可以稍后重试Filter。

过滤器的设置与Servlet的设置类似。@WebFilter中的filterName设置过滤器名称,urlPatterns设置哪些URL请求必须应用哪个过滤器,可应用的URL模式与Servlet基本上相同,而“/*”表示应用在所有的URL请求,过滤器还必须实现Filter接口。

如果要在web.xml中设置,则可以如下所示,标注的设置会被web.xml中的设置覆盖:

...
<filter>
    <filter-name>performance</filter-name>
    <filter-class>cc.openhome.PerformanceFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>performance</filter-name>
    <url-pattern>/*</url-parttern>
</filter-mapping>
...

在过滤器的请求应用上,除了指定URL模式之外,也可以指定Servlet名称,这可以通过@WebServlet的servletName来设置:

@WebFilter(filterName="performance", servletName={"SomeServlet})

或在web.xml的<filter-mapping>中使用<servlet-name>来设置:

...
<filter-mapping>
    <filter-name>performance</filter-name>
    <servlet-name>SomeServlet</servlet-name>
</filter-mapping>
...

如果在过滤器初始化时,想要读取一些参数,可以在@WebFilter中使用@WebInitParam设置InitParam

若要在web.xml中设置过滤器的初始参数,可以在<filter>标签中使用<init-param>进行设置,web.xml中的设置会覆盖标注的设置。

触发过滤器的时机,默认是浏览器直接发出请求。如果是那些通过RequestDispatcher的forward()或include()的请求,设置@WebFilter的dispatcherTypes。例如:

@WebFilter{
    filtername="some",
    urlPatterns={"/some"},
    dispatcherTypes={
        DispatcherType.FORWARD,
        DispatcherType.INCLUDE,
        DispatcherType.REQUEST,
        DispatcherType.ERROT,DispatcherType.ASYNC
    }
}    

如果不设置任何DispatcherTypes,则默认为REQUEST。FORWARD就是通过RequestDispatcher的forward()而来的请求可以套用过滤器。INCLUDE就是通过RequestDispatcher的include()而来的请求可以套用过滤器。ERROR是指由容器处理例外而转发过来的请求可以触发过滤器。ASYNC是指异步处理的请求可以触发过滤器。

若要在web.xml中设置,则可以使用<dispatcher>标签。例如:

...
<filter-mapping>
    <filter-name>SomeFilter</filter-name>
    <servlet-name>*.do</servlet-name>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
    <dispatcher>ERROR</dispatcher>
    <dispatcher>ASYNC</dispatcher>
</filter-mapping>
...

可以通过<url-pattern>或<servlet-name>来指定,那些URL请求或哪些Servlet可以应用过滤器。如果同时具备<url-pattern>与<servlet-name>,则先比对<url-pattern>,再比对<servlet-name>。如果有某个URL或Servlet会应用多个过滤器,则根据<filter-mapping>在web.xml中出现的先后顺序,来决定过滤器的运行顺序。

3.3请求封装器

例子:说明请求封装器的实现与应用,分别是字符替换过滤器与编码设置过滤器。

1.实现字符替换过滤器

假设有个留言板程序已经上线并正常运行中,但是现在发现,有些用户会在留言中输入一些HTML标签。基于安全性的考量,不希望用户输入的HTML标签直接出现在留言中而被浏览器当作HTML的一部分。例如,并不希望用户在留言中输入<a href="http://openhome.cc">OpenHome.cc</a>这样的信息,你不想信息在留言显示中直接变成超链接,让用户有机会在留言板中打广告。

希望将一些HTML过滤掉,如将<、>角括号置换为HTML实体字符&lt;与&gt;。如果不想直接修改留言板程序,则可以使用过滤器的方式,将用户请求参数中的角括号字符进行替换。但问题在于,虽然可以使用HttpServletRequest的getParameter()取得请求参数值,但就是没有一个像setParameter()的方法,可以将处理过后的请求参数重新设置给HttpServletRequest。

对于容器产生的HttpServletRequest对象,无法直接修改某些信息,如请求参数值就是一个例子。你也许会想要亲自实现HttpServletRequest接口,让getParamter()返回过滤后的请求参数值,但这么做的话,HttpServletRequest接口定义的方法都要实现,实现所有方法非常麻烦。

所幸,有个HttpServletRequestWrapper帮你实现了HttpServletRequest接口,只要继承HttpServletRequestWrapper类,并编写想要重新定义的方法即可。相对应于ServletRequest接口,也有个ServletRequestWrapper类可以使用。

2.实现编码设置过滤器

在先前的范例中,如果设置请求字符编码,都是在个别的Servlet中处理。可以在过滤器中进行字符编码设置,如果日后要改变编码,就不用每个Servlet逐一修改设置。

HttpServletRequest的serCharacterEncoding()方法是针对请求Body内容,对于GET请求,必须取得请求参数的字节阵列后,重新指定编码构建字符串。

3.4响应封装器

在Servlet中,是通过HttpServletResponse对象来对浏览器进行响应。如果想要对响应的内容进行压缩处理,就要想办法让HttpRequestResponse对象具有压缩处理的功能。

4、异步处理

Web容器会为每个请求分配一个线程,默认情况下,响应完成前,该线程占用的资源都不会被释放。若有些请求需要长时间处理(例如长时间运算、等待某个资源),就会长时间占用线程所需资源,若这类请求很多,许多线程资源都被长时间占用,会对系统的性能造成负担。

Servlet 3.0新增了异步处理,可以先释放容器分配给请求的线程与相关资源,减轻系统负担,原先释放了容器所分配线程的请求,其响应将被延后,可以在处理完成(如长时间运算完成、所需资源已获得)时再对客户端进行响应。

4.1AsyncContext简介

为了支持异步处理,在Servlet3.0中,在ServletRequest上提供了startAsync()方法:

AsyncContext startAsync() throw java.lang.IllegalStateException;
AsyncContext startAsync(ServletRequest servletRequest,
                    ServletResponse servletResponse)
                   throws java.lang.IllegaStateException

这两个方法都会返回AsyncContext接口的实现对象,前者会直接利用原有的请求与响应对象来创建AsyncContext,后者可以传入自行创建的请求、响应封装对象。在调用startAsync()方法取得AsyncContext对象之后,此次请求的响应会被延后,并释放容器分配的线程。

可以通过AsyncContext的getRequest()getResponse()方法取得请求、响应对象,此次对客户端的响应将暂缓至调用AsyncContext的complete()dispatch()方法为止,前者表示响应完成,后者表示将调派指定的URL响应。

若要能调用ServletRequest的startAsync()以取得AsyncContext,必须告知容器此Servlet支持异步处理,如果使用@WebServlet来标注,则可以设置其asyncSupported为true。

如果使用web.xml设置Servlet,则可以在<servlet>中设置<async-supported>标签为true。

如果Servlet将会进行异步处理,若其前端有过滤器,则过滤器亦需标示其支持异步处理,如果使用@WebFilter,同样可以设置其asyncSupported为true。

如果使用web.xml设置过滤器,则可以设置<async-supported>标签为true。

4.2模拟服务器推播

HTTP是基于请求、响应模型,HTTP服务器无法直接对客户端(浏览器)传送信息,因为没有请求就不会有响应。在这种请求、响应模型下,如果客户端想要获得服务器端应用程序的最新状态,必须以定期(或不定期)方式发送请求,查询服务器端的最新状态。

持续发送请求以查询服务器端最新状态,这种方式的问题在于耗用网络流量,如果多次请求过程后,服务器端应用程序状态并没有变化,那这多次的请求耗用的流量就是浪费的。一个解决的方式是,服务器端将每次请求的响应延后,直到服务器端应用程序状态有变化时再进行响应。当然这样的话,客户端将会处于等待响应状态,如果是浏览器,可以搭配Ajax异步请求技术,而用户将不会因此而被迫停止网页的操作。然而服务器端延后请求的话,若是Servlet/JSP技术,等于该请求占用一个线程,若客户端很多,每个请求都占用线程,将会使得服务器端的性能负担很重。

Servlet3.0中提供的异步技术,可以解决每个请求占用线程的问题,若搭配浏览器端Ajax异步请求技术,就可达到类似服务器端主动通知浏览器的行为,也就是所谓的服务器端推播(Server push)。

例子:模拟应用程序不定期产生最新数据。这个部分由实现ServletContentListener的类负责,会在应用程序启动时进行:

package cc.openhome;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.AsyncContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class WebInitListener implements ServletContextListener{
	private List<AsyncContext>asyncs = new ArrayList<AsyncContext>();
	
	@Override
	public void contextInitialized(ServletContextEvent sce) {
		sce.getServletContext().setAttribute("asyncs", asyncs);
		new Thread(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				while(true) {
					try {
						Thread.sleep((int)(Math.random()*10000));
						double num = Math.random() * 10;
						synchronized(asyncs) {
							for(AsyncContext ctx : asyncs) {
								ctx.getResponse().getWriter().println(num);
								ctx.complete();
							}
							asyncs.clear();
						}
					}catch (Exception e) {
						// TODO: handle exception
						throw new RuntimeException(e);
					}
				}	
			}
		}).start();
	}
	
	@Override
	public void contextDestroyed(ServletContextEvent sce) {};
}

在这个ServletContextListener中,有个List会储存所有异步请求的AsyncContext,并在不定时产生数字后,逐一对客户端响应,并调用AsyncContext的complete()来完成请求。

负责接受请求的Servlet,一收到请求,就将之加入List中:

package cc.openhome;

import java.io.IOException;
import java.util.List;

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;

/**
 * Servlet implementation class AsyncNumServlet
 */
@WebServlet(name="AsyncNumServlet", urlPatterns= {"/asyncNum.do"},
				asyncSupported=true)
public class AsyncNumServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	
	private List<AsyncContext> asyncs;
	
	@SuppressWarnings("unchecked")
	@Override
	public void init() {
		asyncs = (List<AsyncContext>)getServletContext().getAttribute("asyncs");
	}
       
        /**
         * @see HttpServlet#HttpServlet()
         */
        public AsyncNumServlet() {
            super();
            // TODO Auto-generated constructor stub
        }

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
		//response.getWriter().append("Served at: ").append(request.getContextPath());
		AsyncContext ctx = request.startAsync();
		synchronized(asyncs) {
			asyncs.add(ctx);
		}
	}

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
		doGet(request, response);
	}

}

由于维护AsyncContext的List是储存为ServletContext属性,所有在这个Servlet中,必须从Servlet中取出,在每次请求来到时,调用HttpServletRequest的startAsync()进行异步处理,并将取得AsyncContext加入至维护AsyncContext的List中。

测试HTML:async.html

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>实时资料</title>
<script type="text/javascript">
	function asyncUpdate() {
		var xhr;
		if(window.XMLHttpRequest){
			xhr = new XMLHttpRequest();
		}else if(window.ActiveXObject){
			xhr = new ActiveXObject('Microsoft.XMLHTTP');
		}
		
		xhr.onreadystatechange = function(){
			if(xhr.readyState === 4){
				if(xhr.status === 200){
					document.getElementById('data').innerHTML = xhr.responseText;
					asyncUpdate();
				}
			}
		};
		xhr.open('GET', 'asyncNum.do?timestamp='+new Date().getTime());
		xhr.send(null);
	}
	window.onload = asyncUpdate;
</script>
</head>
<body>
实时资料:<span id="data">0</span>
</body>
</html>

4.3更多AsyncContext细节

如果Servlet或过滤器的asyncSupported被标示为true,则它们支持异步请求处理,在不支持异步处理的Servlet或过滤器中调用startAsync(),会抛出IllegalStateException。

当在支持异步处理的Servlet或过滤器中调用请求对象的startAsync()方法时,该次请求会离开容器所分配的线程,这意味着必须响应处理流程会返回,也就是若有过滤器,也会依序返回(也就是各自完成FilterChain的doFilter()方法),但最终的响应会被延迟。

可以调用AsyncContext的complete()方法完成响应或调用forward()方法,将响应转发给别的Servlet/JSP处理,AsyncContext的forward()将请求的响应权派送给别的页面处理,给定的路径是相对于ServletContext的路径。不可以自行在同一个AsyncContext上同时调用complete()与forward(),否则会抛出IllegalStateException。

不可以在两个异步处理的Servlet间派送前,连续调用两次startAsync(),否则会抛出IllegalStateException。

将请求从支持异步处理的Servlet(asyncSupported被标示为true)派送至一个同步处理的Servlet是可行的(asyncSupported被标示为false),此时容器会负责调用AsyncContext的complete()。

如果一个同步处理的Servlet派送至一个支持异步处理的Servlet,在异步处理中调用AsyncContext的startAsync(),将会抛出IllegalStateException。

如果对AsyncContext的起始、完成、超时或错误发生等事件感兴趣,可以实现AsyncListener。其定义如下:

package javax.servlet;
import java.io.IOException;
import java.util.EventListener;
public interface AsyncListenner extends EventListener{
    void onComplete(AsyncEvent event) throws IOException;
    void onTimeOut(AsyncEvent event) throws IOException;
    void onError(AsyncExent event) throws IOException;
    void onStartAsync(AsyncEvent event) throw IOException;
}

AsyncContext有个addListener()方法,可以加入AsyncListener的实现对象,在对应事件发生时会调用AsyncListener实现对象的对应方法。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值