Java EE 程序设计
上QQ阅读APP看书,第一时间看更新

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 网站计数器显示信息