
3.11 Listener
Listener(监听器)是Servlet 2.4规范以后增加的新特性。Listener用来主动监听Web容器事件。所谓Web容器事件,是指Web应用上下文创建销毁、会话对象创建销毁以及会话属性信息的增删改等。通过事件监听,Listener对象可以在事件发生前、发生后进行一些必要的处理。Listener实现了Web应用的事件驱动,使得Web应用不仅可以被动地处理客户端发出的请求,而且可以主动对Web容器的变化进行响应,大大提高了Web应用的能力。
为了实现事件监听功能,Listener必须实现Listener接口,同时,代表Web容器事件的Event类作为参数传递到Listener接口,Listener可以通过它来对Web容器事件进行必要的处理。
目前Servlet规范共有7个Listener接口和5个Event类,Event类与Listener之间的关系如表3-3所示。
表3-3 Servlet规范中支持的Listener接口和Event类

注:新的Java EE规范不再支持ServletRequestListener和ServletRequestAttributeListener接口。
1.ServletContextListener和ServletContextEvent
ServletContextEvent用来代表Web应用上下文事件。ServletContextListener用于监听Web应用上下文事件。当Web应用启动时ServletContext被创建或当Web应用关闭时ServletContext将要被销毁,Web容器都将发送ServletContextEvent事件到实现了ServletContextListener接口的对象实例。
实现ServletContextListener接口的实例必须实现以下接口方法:
·void contextInitialized(ServletContextEvent sce)——通知Listener对象,Web应用已经被加载及初始化。
·void contextDestroyed(ServletContextEvent sce)——通知Listener对象,Web应用已经被销毁。
ServletContextEvent中最常用的方法为:
·ServletContext getServletContext()——取得ServletContext对象。
Listener通常利用此方法来获取Servlet上下文信息,然后进行相应的处理。
2.ServletContextAttributeListener和ServletContextAttributeEvent
ServletContextAttributeEvent代表Web上下文属性事件,它包括增加属性、删除属性、修改属性等。ServletContextAttributeListener用于监听上述Web上下文属性事件。
ServletContextAttributeListener接口主要有以下方法:
·void attributeAdded(ServletContextAttributeEvent scab)——当有对象加入Application的范围,通知Listener对象。
·void attributeRemoved(ServletContextAttributeEvent scab)——若有对象从Application的范围移除,通知Listener对象。
·void attributeReplaced(ServletContextAttributeEvent scab)——若在Application的范围中,有对象取代另一个对象时,通知Listener对象。
ServletContextAttributeEvent中常用的方法如下:
·java.lang.String getName()——返回属性的名称。
·java.lang.Object getValue()——返回属性的值。
3.HttpSessionBindingListener和HttpSessionBindingEvent
HttpSessionBindingEvent代表会话绑定事件。当对象加入Session范围(即调用HttpSession对象的setAttribute方法的时候)或从Session范围中移除(即调用HttpSession对象的removeAttribute方法的时候或Session Time out的时候)时,都将触发该事件,此时,Web容器将发送消息给实现了HttpSessionBindingListener接口的对象。
实现了HttpSessionBindingListener接口的对象必须实现以下两个方法:
·void valueBound(HttpSessionBindingEvent event)——通知Listener,有新的对象加入Session。
·void valueUnbound(HttpSessionBindingEvent event)——通知Listener,有对象从Session中删除。
4.HttpSessionAttributeListener和HttpSessionBindingEvent
HttpSessionAttributeListener也是用来监听HttpSessionBindingEvent事件,但是实现HttpSessionAttributeListener接口的对象必须实现以下不同的接口方法:
·attributeAdded(HttpSessionBindingEvent se)——当在Session增加一个属性时,Web容器调用此方法。
·attributeRemoved(HttpSessionBindingEvent se)——当在Session删除一个属性时,Web容器调用此方法。
·attributeReplaced(HttpSessionBindingEvent se)——当在Session属性被重新设置时,Web容器调用此方法。
说明:HttpSessionAttributeListener和HttpSessionBindingListener的区别是HttpSession-AttributeListener是从会话的角度去观察,而HttpSessionBindingListener是从对象绑定的角度来观察。当会话超时或无效时,对象会从会话解除绑定。此时HttpSessionBindingListener会得到通知,而HttpSessionAttributeListener则不会。
5.HttpSessionListener和HttpSessionEvent
HttpSessionEvent代表HttpSession对象的生命周期事件包括HttpSession对象的创建、销毁等。HttpSessionListener用来对HttpSession对象的生命周期事件进行监听。
实现HttpSessionListener接口的对象必须实现以下接口方法:
·sessionCreated(HttpSessionEvent se)——当创建一个Session时,Web容器调用此方法。
·sessionDestroyed(HttpSessionEvent se)——当销毁一个Session时,Web容器调用此方法。
6.HttpSessionActivationListener接口
主要用于服务器集群的情况下,同一个Session转移至不同的JVM的情形。此时,实现了HttpSessionActivationListener接口的Listener将被触发。
7.AsyncListener
对Servlet异步处理事件提供监听,实现AsyncListener接口的对象必须实现以下接口方法:
·onStartAsync(AsyncEvent event)——异步线程开始时,Web容器调用此方法。
·onError(AsyncEvent event)——异步线程出错时,Web容器调用此方法。
·onTimeout(AsyncEvent event)——异步线程执行超时,Web容器调用此方法。
·onComplete(AsyncEvent event)——异步线程执行完毕时,Web容器调用此方法。
要注册一个AsyncListener,只需将准备好的AsyncListener对象作为参数传递给AsyncContext对象的addListener()方法即可,如下列代码片段所示:
AsyncContext ctx = req.startAsync(); ctx.addListener(new AsyncListener() { public void onComplete(AsyncEvent asyncEvent) throws IOException { // 做一些清理工作或者其他 } ... });
利用上述七类Listener接口,Web应用实现了对Web容器的会话以及应用上下文层面上的事件的监听处理。
除HttpSessionBindingListener接口和AsyncListener接口,其他所有关于Listener的配置信息都存储在Web应用的部署描述文件Web.xml中,Web容器通过此文件中的信息来决定当某个特定事件发生时,将自动创建对应的Listener对象的实例并调用相应的接口方法进行处理。
下面通过创建一个网站计数器来演示如何应用Listener来开发Web应用。网站计数器要求满足以下功能:
(1)统计应用自部署以来的所有用户访问次数。
(2)对于用户在一次会话中的访问只记录一次,以保证数据的真实性。
(3)统计在线用户数量信息。
由于网站计数器要记录应用自部署以来的所有用户访问次数,因此,必须将用户访问信息实现持久化。由于用户访问信息比较简单,因此,可以将信息持久化存储到外部的资源文件中,而不是数据库。
利用ServletContextListener控制历史计数信息的读取和写入。当Web应用上下文创建时,将历史计数信息从外部资源文件读取到内存。当Web应用上下文关闭时,则将历史计数信息持久化存储到外部资源文件中。
利用HttpSessionListener监听在线用户数量变化。每创建一个新的会话,则代表产生一次新的用户访问。每一个会话销毁事件,则代表一位用户离线。这种方式也避免了用户重复刷新导致的重复计数。
首先在Web应用的文件夹“Web页”下建立一个名为Count.txt的空文件,用来存储历史访问数据。
下面在Web应用Chapter3中创建一个辅助工具类CounterFile来实现对资源文件的操作。代码如程序3-35所示。
程序3-35:CounterFile.java
package com.example; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; //用来操作记录访问次数的文件 public class CounterFile { private BufferedReader file; //BufferedReader对象,用于读取文件数据 public CounterFile() { } //ReadFile方法用来读取文件filePath中的数据,并返回这个数据 public String ReadFile(String filePath) throws FileNotFoundException { String currentRecord = null; //保存文本的变量 //创建新的BufferedReader对象 file = new BufferedReader(new FileReader(filePath)); String returnStr =null; try { //读取一行数据并保存到currentRecord变量中 currentRecord = file.readLine(); } catch (IOException e) {//错误处理 System.out.println("读取数据错误."); } if (currentRecord == null) //如果文件为空 returnStr = "没有任何记录"; else {//文件不为空 returnStr =currentRecord; } //返回读取文件的数据 return returnStr; } //ReadFile方法用来将数据counter+1后写入到文本文件filePath中 //以实现计数增长的功能 public synchronized void WriteFile(String filePath, String counter) throws FileNotFoundException { int Writestr = 0; Writestr=Integer.parseInt(counter); try { //创建PrintWriter对象,用于写入数据到文件中 PrintWriter pw = new PrintWriter(new FileOutputStream(filePath)); //用文本格式打印整数Writestr pw.println(Writestr); //清除PrintWriter对象 pw.close(); } catch(IOException e) { //错误处理 System.out.println("写入文件错误"+e.getMessage()); } } }
程序说明:程序用来实现对资源文件Count.txt的读写操作。其中方法ReadFile(String filePath)负责将信息从资源文件读取到内存,方法WriteFile(String filePath, String counter)负责将计数信息写回到资源文件。为了避免写文件时发生线程安全问题,这里将方法WriteFile用修饰符synchronized加以保护。
下面创建一个ServletContextListener来实现对Web应用创建、销毁事件的监听,以便在Web应用创建或销毁时从资源文件载入或回写历史访问数据。在“项目”视图中选中Web应用Chapter3,右击,在弹出的快捷菜单中选择“新建”→“Web应用程序监听程序”命令,弹出“New Web应用程序监听程序”对话框,如图3-45所示。

图3-45 “New Web应用程序监听程序”对话框
在“类名”文本框中输入辅助工具类名CountListener,在“包”文本框中输入Servlet类所在的包名com.servlet,在“要实现的接口”列表中选中“上下文监听程序”,单击“完成”按钮,NetBeans自动生成类CountListener的框架源文件。代码如程序3-36所示。
程序3-36:CounterListener.java
package com.example; import javax.servlet.ServletContextListener; import javax.servlet.ServletContextEvent; public class CounterListener implements ServletContextListener { String path=""; public void contextInitialized(ServletContextEvent evt) { CounterFile f=new CounterFile(); String name=evt.getServletContext().getInitParameter("CounterPath"); path=evt.getServletContext().getRealPath(name); try{ String temp=f.ReadFile(path); System.out.println(temp); //将计数器的值放入应用上下文 evt.getServletContext().setAttribute("Counter", temp); }catch(Exception e){ System.out.println(e.toString()); } } public void contextDestroyed(ServletContextEvent evt) { try{ String current= (String)evt.getServletContext().getAttribute ("Counter"); CounterFile f=new CounterFile(); f.WriteFile(path, current); }catch(Exception e){ System.out.println(e.toString()); } } }
程序说明:程序调用辅助工具类CounterFile来操作资源文件。在contextInitialized(ServletContextEvent evt)方法中将历史计数信息从外部读入到内存,在contextDestroyed(ServletContextEvent evt)方法中将历史计数信息持久化储存到外部文件。这样,在程序运行期间,不管应用的访问次数多么频繁,所用计数操作只是操作内存中的变量,应用程序的性能将不会因为频繁的IO操作而下降。
计数器文件的路径信息是从Web应用的上下文中读取的,因此在运行程序之前,要在Web应用上下文中添加一个名为CounterPath的上下文参数。打开Web.xml,切换到“常规”视图,单击“上下文参数”条目下的“添加”按钮,弹出“添加上下文参数”对话框,如图3-46所示。

图3-46 “添加上下文参数”对话框
在“参数名称”文本框中输入CounterPath,在“参数值”中输入count.txt,单击“确定”按钮,Web上下文参数添加完毕。查看web.xml的源代码,可以看到添加的上下文参数和监听器CounterListener配置信息如程序3-37中斜体部分所示。
程序3-37:Web.xml(部分)
… <context-param> <param-name>CounterPath</param-name> <param-value>count.txt</param-value> </context-param> … <listener> <description>ServletContextListener</description> <listener-class>com.servlet.CounterListener</listener-class> </listener> …
为了监听在线用户数目,还要创建一个实现HttpSessionListener接口的Listener。在“项目”视图中选中Web应用Chapter 3,右击,在弹出的快捷菜单中选择“新建”→“Web应用程序监听程序”,弹出“New Web应用程序监听程序”对话框,如图3-47所示。

图3-47 “New Web应用程序监听程序”对话框
在“类名”文本框中输入类名SessionListener,在“包”文本框中输入Servlet类所在的包名com.servlet,在“要实现的接口”列表中选中“HTTP会话监听程序”,单击“完成”按钮,Netbeans自动生成类SessionListener的框架源文件。完整源代码如程序3-38所示。
程序3-38:SessionListener.java
package com.example; import javax.servlet.http.HttpSessionListener; import javax.servlet.http.HttpSessionEvent; @WebListener( ) public class SessionListener implements HttpSessionListener { public void sessionCreated(HttpSessionEvent evt) { // 修改在线人数 String current= (String)evt.getSession().getServletContext(). getAttribute("online"); if(current==null)current="0"; int c=Integer.parseInt(current); c++; current=String.valueOf(c); evt.getSession().getServletContext().setAttribute("online", current); //修改历史人数 String his= (String)evt.getSession().getServletContext(). getAttribute("Counter"); if(his==null)his="0"; int total=Integer.parseInt(his)+1; his=String.valueOf(total); evt.getSession().getServletContext().setAttribute("Counter", his); } public void sessionDestroyed(HttpSessionEvent evt) { // TODO在此处添加您的代码: // 修改在线人数 String current= (String)evt.getSession().getServletContext().getAttribute ("online"); if(current==null)current="0"; int c=Integer.parseInt(current); c--; current=String.valueOf(c); evt.getSession().getServletContext().setAttribute("online", current); } }
程序说明:程序通过监听会话事件来维护在线人数和历史访问次数信息。在sessionCreated(HttpSessionEvent evt)方法中,从Web应用上下文中获取历史计数信息和在线人数信息,并分别增加1。在sessionDestroyed(HttpSessionEvent evt)方法中,从Web应用上下文中获取在线人数信息,并减1。
最后创建一个Servlet来显示在线用户数量以及历史用户数量。代码如程序3-39所示。
程序3-39:Counter.java
package com.servlet; … public class Counter extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html; charset=UTF-8"); PrintWriter out = response.getWriter(); String dumb=(String)request.getSession().getAttribute("dumb"); //触发session事件 String history =(String)getServletContext().getAttribute("Counter"); if( history==null) history="0"; String temp =(String)getServletContext().getAttribute("online"); if(temp==null)temp="0"; out.println("<html>"); out.println("<head>"); out.println("<title>计数器</title>"); out.println("</head>"); out.println("<body>"); out.println("<h1>当前访问人数:" + temp + "</h1>"); out.println("<h1>历史访问人数:" + history + "</h1>"); out.println("</body>"); out.println("</html>"); out.close(); } … }
程序说明:从Web应用上下文属性中获取历史访问次数和在线人数信息,然后通过Servlet输出到页面。
程序发布成功后,在浏览器地址栏输入http://localhost:8080/Chapter3/Counter,将得到如图3-48所示的运行页面。

图3-48 网站计数器显示信息