Java Web 之 Listener

本文包括:

1、Listener簡介

2、Servlet監聽器

3、監聽三個域對象創建和銷毀的事件監聽器

4、監聽三個域對象的屬性(Attribute)的變化的事件監聽器

5、監聽綁定到 HttpSession 域中的某個對象的狀態的事件監聽器

1、Listener簡介

  • Listener(監聽器)就是一個實現特定接口的普通java程序,這個程序專門用于監聽另一個java對象的方法調用或屬性改變,當被監聽對象發生上述事件后,監聽器某個方法將立即被執行。

  • 為了加深理解,自定義監聽器來練練手,假設現在有個體重100的人要吃飯了,要監聽他吃飯的動作,捕捉到了之后再打印它的體重,具體思路如下;

    1. 事件源類:

       public class Person {
           private String name;
           private int weight;// 體重
           public String getName() {
               return name;
           }
       
           public void setName(String name) {
               this.name = name;
           }
       
           public int getWeight() {
               return weight;
           }
       
           public void setWeight(int weight) {
               this.weight = weight;
           }
       
       }
      
    2. 監聽器接口:

       public interface PersonListener {
           public void personeating(PersonEvent event);// 監聽方法,需要一個事件對象作為參數
       }
      
    3. 事件類:

       public class PersonEvent {
           private Object source;// 事件源
       
           public Object getSource() {
               return source;
           }
       
           public void setSource(Object source) {
               this.source = source;
           }
       
           // 提供一個這樣的構造方法:構造事件對象時,接收事件源(Person)
           public PersonEvent(Person person) {
               this.source = person;
           }
       
       }
      
    4. 在事件源中注冊監聽器:

       public class Person {
           private String name;
           private int weight;// 體重
           private PersonListener listener;
       
           // 注冊監聽器
           public void addPersonListener(PersonListener listener) {
               this.listener = listener;
           }
           public String getName() {
               return name;
           }
       
           public void setName(String name) {
               this.name = name;
           }
       
           public int getWeight() {
               return weight;
           }
       
           public void setWeight(int weight) {
               this.weight = weight;
           }
       
       }
      
    5. 操作事件源 ----- 在事件源方法中,構造事件對象,參數為當前事件源(this),傳遞事件對象給監聽器的監聽方法:

       public class Person {
           private String name;
           private int weight;// 體重
           private PersonListener listener;
       
           // 注冊監聽器
           public void addPersonListener(PersonListener listener) {
               this.listener = listener;
           }
       
           // 吃飯
           public void eat() {
               // 體重增加
               weight += 5;
               // 調用監聽器監聽方法
               if (listener != null) {
                   // 監聽器存在
                   // 創建事件對象 --- 通過事件對象可以獲得事件源
                   PersonEvent event = new PersonEvent(this);
       
                   listener.personeating(event);
               }
           }
       
           public String getName() {
               return name;
           }
       
           public void setName(String name) {
               this.name = name;
           }
       
           public int getWeight() {
               return weight;
           }
       
           public void setWeight(int weight) {
               this.weight = weight;
           }
       
       }
      
    6. 測試:

       public class PersonTest {
           public static void main(String[] args) {
               // 步驟一 創建事件源
               Person person = new Person();
               person.setName("小明");
               person.setWeight(100);
       
               // 步驟二 給事件源注冊監聽器(該監聽器由匿名內部類創建)
               person.addPersonListener(new PersonListener() {
                   @Override
                   public void personeating(PersonEvent event) {
                       System.out.println("監聽到了,人正在吃飯!");
                       
                       // 在監聽方法中可以獲得事件源對象,進而可以操作事件源對象
                       Person person = (Person) event.getSource();
                       System.out.println(person.getName());
                       System.out.println(person.getWeight());
                   }
               });
       
               // 步驟三 操作事件源
               person.eat();// 結果監聽方法被調用
           }
       }
      

2、Servlet監聽器

  • 在Servlet規范中定義了多種類型的監聽器,它們用于監聽的事件源是三個域對象,分別為:

    • ServletContext

    • HttpSession

    • ServletRequest

  • Servlet規范針對這三個域對象上的操作,又把這多種類型的監聽器劃分為三種類型:

    • 監聽三個域對象的創建和銷毀的事件監聽器

    • 監聽三個域對象的屬性(Attribute)的增加和刪除的事件監聽器

    • 監聽綁定到 HttpSession 域中的某個對象的狀態的事件監聽器。(查看API文檔)

  • 編寫 Servlet 監聽器:

    • 和編寫其它事件監聽器一樣,編寫Servlet監聽器也需要實現一個特定的接口,并針對相應動作覆蓋接口中的相應方法。

    • 和其它事件監聽器略有不同的是,Servlet監聽器的注冊不是直接注冊在事件源上,而是由WEB容器負責注冊,開發人員只需在web.xml文件中使用<listener>標簽配置好監聽器,web容器就會自動把監聽器注冊到事件源中。

    • 一個 web.xml 文件中可以配置多個 Servlet 事件監聽器,web 服務器按照它們在 web.xml 文件中的注冊順序來加載和注冊這些 Serlvet 事件監聽器。配置代碼如下所示:

        <!-- 對監聽器進行注冊 -->
        <!-- 監聽器和Servlet、Filter不同,不需要url配置,監聽器執行不是由用戶訪問的,監聽器 是由事件源自動調用的 -->
        <listener>
            <listener-class>cn.itcast.servlet.listener.MyServletContextListener</listener-class>
        </listener>
      

3、監聽三個域對象創建和銷毀的事件監聽器

3.1、ServletContextListener

  • ServletContextListener 接口用于監聽 ServletContext 對象的創建和銷毀事件。

    • 當 ServletContext 對象被創建時,調用接口中的方法:

        ServletContextListener.contextInitialized (ServletContextEvent sce)
      
    • 當 ServletContext 對象被銷毀時,調用接口中的方法:

        ServletContextListener.contextDestroyed(ServletContextEvent sce)
      
  • ServletContext域對象何時創建和銷毀:

    • 創建:服務器啟動時,針對每一個web應用創建Servletcontext

    • 銷毀:服務器關閉前,先關閉代表每一個web應用的ServletContext

  • ServletContext主要用來干什么?

    1. 保存全局應用數據對象

      • 在服務器啟動時,對一些對象進行初始化,并且將對象保存ServletContext數據范圍內 —— 實現全局數據

      • 例如:創建數據庫連接池

    2. 加載框架配置文件

      • Spring框架(配置文件隨服務器啟動加載) org.springframework.web.context.ContextLoaderListener
    3. 實現任務調度(定時器),啟動定時程序

      • java.util.Timer:一種線程設施,用于安排以后在后臺線程中執行的任務,可安排任務執行一次,或者定期重復執行。

      • Timer提供了啟動定時任務方法 Timer.schedule(),其中有兩種方法需要記住:

        1. 在指定的一個時間時啟動定時器,定期執行一次

            Timer.schedule(TimerTask task, Date firstTime, long period)  
          
        2. 在當前時間延遲多少毫秒后啟動定時器,定期執行一次

            Timer.schedule(TimerTask task, long delay, long period)  
          
      • 停止定時器,取消任務

          Timer.cancel() 
        
  • demo:

      package cn.itcast.servlet.listener;
      
      import java.text.DateFormat;
      import java.text.ParseException;
      import java.text.SimpleDateFormat;
      import java.util.Date;
      import java.util.Timer;
      import java.util.TimerTask;
      
      import javax.servlet.ServletContext;
      import javax.servlet.ServletContextEvent;
      import javax.servlet.ServletContextListener;
      
      /**
       * 自定義 Context監聽器
       * 
       * @author seawind
       * 
       */
      public class MyServletContextListener implements ServletContextListener {
      
          @Override
          public void contextDestroyed(ServletContextEvent sce) {
              System.out.println("監聽ServletContext對象銷毀了...");
          }
      
          @Override
          public void contextInitialized(ServletContextEvent sce) {
              System.out.println("監聽ServletContext對象創建了...");
              // 獲得事件源
              ServletContext servletContext = sce.getServletContext();
              // 向ServletContext 中保存數據
      
              // 啟動定時器
              final Timer timer = new Timer();
              // 啟動定時任務
      
              // timer.schedule(new TimerTask() {
              // @Override
              // // 這就是一個線程
              // public void run() {
              // System.out.println("定時器執行了...");
              // }
              // }, 0, 3000); // 馬上啟動 每隔3秒重復執行
      
              // 指定時間啟動定時器
              DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
              try {
                  Date first = dateFormat.parse("2012-08-07 10:42:00");
                  timer.schedule(new TimerTask() {
                      int i;
      
                      @Override
                      public void run() {
                          i++;
                          System.out.println("從10點40分開始啟動程序,每隔3秒重復執行");
                          if (i == 10) {
                              timer.cancel();// 取消定時器任務
                          }
                      }
                  }, first, 3000);
              } catch (ParseException e) {
                  e.printStackTrace();
              }
          }
      
      }
    

3.2、HttpSessionListener

  • HttpSessionListener接口用于監聽HttpSession的創建和銷毀

    • 創建一個Session時,接口中的該方法將會被調用:

        HttpSessionListener.sessionCreated(HttpSessionEvent se) 
      
    • 銷毀一個Session時,接口中的該方法將會被調用:

        HttpSessionListener.sessionDestroyed (HttpSessionEvent se) 
      
  • Session域對象創建和銷毀:

    • 創建:瀏覽器訪問服務器時,服務器為每個瀏覽器創建不同的 session 對象,服務器創建session

    • 銷毀:如果用戶的session的30分鐘沒有使用,session就會過期,我們在Tomcat的web.xml里面也可以配置session過期時間。

  • demo:

      package cn.itcast.servlet.listener;
      
      import javax.servlet.http.HttpSession;
      import javax.servlet.http.HttpSessionEvent;
      import javax.servlet.http.HttpSessionListener;
      
      public class MyHttpSessionListener implements HttpSessionListener {
      
          @Override
          public void sessionCreated(HttpSessionEvent se) {
              // 通過事件對象獲得session 的id 
              System.out.println("session被創建了");
              HttpSession session = se.getSession();
              System.out.println("id:" + session.getId());
          }
      
          @Override
          public void sessionDestroyed(HttpSessionEvent se) {
              System.out.println("session被銷毀了");
              HttpSession session = se.getSession();
              System.out.println("id:" + session.getId());
          }
      
      }
    

關于HttpSession的生命周期、具體描述詳見:JSP 會話管理

3.3、ServletRequestListener(很少用)

  • ServletRequestListener 接口用于監聽ServletRequest 對象的創建和銷毀:

    • ServletRequest對象被創建時,監聽器的requestInitialized方法將會被調用。

    • ServletRequest對象被銷毀時,監聽器的requestDestroyed方法將會被調用。

  • ServletRequest域對象創建和銷毀的時機:

    • 創建:用戶每一次訪問,都會創建一個reqeust

    • 銷毀:當前訪問結束,request對象就會銷毀

  • 這個監聽器最需要注意的:

    • 使用forward ---- request創建銷毀一次 (因為轉發本質是一次請求)

    • 使用sendRedirect ---- request創建銷毀兩次 (因為重定向本質是兩次請求)

關于ServletRequest詳見:http://www.lxweimin.com/p/7e2e3fd58e91

3.4、案例:統計在線人數

  • 圖解:

  • 首先,初始化在線人數,根據前文,ServletContextListener可以監聽ServletContext對象的創建,所以新建一個實現監聽器接口的類:

      package cn.itcast.servlet.listener.demo2;
      
      import javax.servlet.ServletContext;
      import javax.servlet.ServletContextEvent;
      import javax.servlet.ServletContextListener;
      
      public class OnlineCountServletContextListener implements
              ServletContextListener {
      
          @Override
          public void contextDestroyed(ServletContextEvent sce) {
          }
      
          @Override
          public void contextInitialized(ServletContextEvent sce) {
              // 初始化在線人數為0
              ServletContext context = sce.getServletContext();
              context.setAttribute("onlinenum", 0);
          }
      
      }
    
  • 利用HttpSessionListener監聽HttpSession對象的創建和銷毀,可以統計在線人數:

      package cn.itcast.servlet.listener.demo2;
      
      import javax.servlet.ServletContext;
      import javax.servlet.http.HttpSession;
      import javax.servlet.http.HttpSessionEvent;
      import javax.servlet.http.HttpSessionListener;
      
      public class OnlineCountHttpSessionListener implements HttpSessionListener {
      
          @Override
          public void sessionCreated(HttpSessionEvent se) {
              // 當Session對象被創建時,在線人數 +1
              HttpSession session = se.getSession();
              ServletContext context = session.getServletContext();
      
              int onlinenum = (Integer) context.getAttribute("onlinenum");
              context.setAttribute("onlinenum", onlinenum + 1);
      
              System.out.println(session.getId() + "被創建了...");
          }
      
          @Override
          public void sessionDestroyed(HttpSessionEvent se) {
              // 當Session對象被銷毀時,在線人數 - 1
              HttpSession session = se.getSession();
              ServletContext context = session.getServletContext();
      
              int onlinenum = (Integer) context.getAttribute("onlinenum");
              context.setAttribute("onlinenum", onlinenum - 1);
      
              System.out.println(session.getId() + "被銷毀了 ...");
          }
      
      }
    
  • 別忘了在web.xml中配置:

      <listener>
          <listener-class>cn.itcast.servlet.listener.demo2.OnlineCountServletContextListener</listener-class>
      </listener>
      <listener>
          <listener-class>cn.itcast.servlet.listener.demo2.OnlineCountHttpSessionListener</listener-class>
      </listener>
    
  • 簡單創建一個JSP頁面(注意,JSP作用域中applicationScope的范圍是整個服務器,所以可以得到ServletContext對象中存儲的值):

      <%@ page language="java" contentType="text/html; charset=UTF-8"
          pageEncoding="UTF-8"%>
      <!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>Insert title here</title>
      </head>
      <body>
      <h1>顯示在線人數</h1>
      ${applicationScope.onlinenum }
      </body>
      </html>
    

3.5、案例:利用定時器定時銷毀Session

  • 圖解:

  • ScannerServletContextListener干了兩件事:初始化List<HttpSession>;啟動定時器,每隔20秒執行一次。

    注意:

    因為要從List中刪除元素,所以循環用Iterator而不用foreach。

    在使用掃描刪除Session對象時,要保證Session的List集合長度不能改變(即此時此刻不能添加新的Session) ---- 利用同步解決 synchronized

      package cn.itcast.servlet.listener.demo3;
      
      import java.util.ArrayList;
      import java.util.Iterator;
      import java.util.List;
      import java.util.Timer;
      import java.util.TimerTask;
      
      import javax.servlet.ServletContext;
      import javax.servlet.ServletContextEvent;
      import javax.servlet.ServletContextListener;
      import javax.servlet.http.HttpSession;
      
      public class ScannerServletContextListener implements ServletContextListener {
      
          @Override
          public void contextDestroyed(ServletContextEvent sce) {
          }
      
          @Override
          public void contextInitialized(ServletContextEvent sce) {
              // 第一件事,創建Session的List集合
              final List<HttpSession> sessionList = new ArrayList<HttpSession>();
              // 將集合保存ServletContext對象
              ServletContext servletContext = sce.getServletContext();
              servletContext.setAttribute("sessionList", sessionList);
      
              // 第二件事,啟動定時器,每隔20秒執行一次
              Timer timer = new Timer();
              timer.schedule(new TimerTask() {
                  @Override
                  public void run() {
                      System.out.println("定時session掃描器執行了....");
                      // 掃描Session的List集合,看哪個Session已經1分鐘沒用了
                      // 發現Session1分鐘沒有使用,銷毀Session 從集合移除
                      synchronized (sessionList) {
                          Iterator<HttpSession> iterator = sessionList.iterator();
                          while (iterator.hasNext()) {
                              HttpSession session = iterator.next();
                              if (System.currentTimeMillis()
                                      - session.getLastAccessedTime() > 1000 * 60) {
                                  System.out.println(session.getId()
                                          + "對象已經1分鐘沒有使用,被銷毀了...");
                                  // 銷毀Session
                                  session.invalidate();
                                  // 從集合移除Session
                                  iterator.remove();
      
                              }
                          }
                      }
                  }
              }, 0, 20000);
          }
      
      }
    
  • ScannerHttpSessionListener:

      package cn.itcast.servlet.listener.demo3;
      
      import java.util.List;
      
      import javax.servlet.ServletContext;
      import javax.servlet.http.HttpSession;
      import javax.servlet.http.HttpSessionEvent;
      import javax.servlet.http.HttpSessionListener;
      
      public class ScannerHttpSessionListener implements HttpSessionListener {
      
          @Override
          public void sessionCreated(HttpSessionEvent se) {
              // 在創建Session對象時,將Session對象加入集合
              HttpSession httpSession = se.getSession();
              ServletContext context = httpSession.getServletContext();
      
              List<HttpSession> sessionList = (List<HttpSession>) context
                      .getAttribute("sessionList");
              synchronized (sessionList) {
                  sessionList.add(httpSession);
              }
      
              // 還是否需要context.setAttribute? ---- 不需要:因為之前得到的是List的地址
              context.setAttribute("sessionList", sessionList); // 這行代碼寫不寫無所謂
      
              System.out.println(httpSession.getId() + "被創建了...");
          }
      
          @Override
          public void sessionDestroyed(HttpSessionEvent se) {
          }
      
      }
    
  • 配置:

      <listener>
          <listener-class>cn.itcast.servlet.listener.demo3.ScannerServletContextListener</listener-class>
      </listener>
      <listener>
          <listener-class>cn.itcast.servlet.listener.demo3.ScannerHttpSessionListener</listener-class>
      </listener>
    

4、監聽三個域對象的屬性(Attribute)的變化的事件監聽器

  • Servlet規范定義了監聽 ServletContext, HttpSession, HttpServletRequest 這三個對象中的屬性(Attribute)變更信息事件的監聽器。

  • 這三個監聽器接口分別是

    • ServletContextAttributeListener

    • HttpSessionAttributeListener

    • ServletRequestAttributeListener

  • 這三個接口中都定義了三個方法來處理被監聽對象中的屬性的增加,刪除和替換的事件,同一個事件在這三個接口中對應的方法名稱完全相同,只是接受的參數類型不同

  • XXListener.attributeAdded(XXEvent)

    • 當向被監聽器對象中增加一個屬性時,web容器就調用事件監聽器的 attributeAdded 方法進行相應,這個方法接受一個事件類型的參數,監聽器可以通過這個參數來獲得正在增加屬性的域對象和被保存到域中的屬性對象

    • 各個域屬性監聽器中的完整語法定義為:

        public void attributeAdded(ServletContextAttributeEvent scae)
        public void attributeAdded (HttpSessionBindingEvent  hsbe) 
        public void attributeAdded(ServletRequestAttributeEvent srae)
      
  • XXListener.attributeRemoved(XXEvent)

    • 當刪除被監聽對象中的一個屬性時,web 容器調用事件監聽器的這個方法進行相應

    • 各個域屬性監聽器中的完整語法定義為:

        public void attributeRemoved(ServletContextAttributeEvent scae) 
        public void attributeRemoved (HttpSessionBindingEvent  hsbe) 
        public void attributeRemoved (ServletRequestAttributeEvent srae)
      
  • XXListener.attributeReplaced(XXEvent)

    • 當監聽器的域對象中的某個屬性被替換時,web容器調用事件監聽器的這個方法進行相應

    • 各個域屬性監聽器中的完整語法定義為:

        public void attributeReplaced(ServletContextAttributeEvent scae) 
        public void attributeReplaced (HttpSessionBindingEvent  hsbe) 
        public void attributeReplaced (ServletRequestAttributeEvent srae)
      
  • 由于這三個監聽器用法極其相似,所以只用一個例子來演示具體用法:

  • 新建有一個JSP頁面:

      <%@ page language="java" contentType="text/html; charset=UTF-8"
          pageEncoding="UTF-8"%>
      <!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>Insert title here</title>
      </head>
      <body>
      <%
          // 向Session數據范圍 保存名稱為"name",值為"張三"的屬性(Attribute)
          session.setAttribute("name","張三"); // 觸發attributeAdd
    
          // 將session中屬性(Attribute)名為"name"的值替換為"李四"
          session.setAttribute("name","李四"); // 觸發attributeReplaced
    
          // 移除名為"name"的屬性
          session.removeAttribute("name");// 觸發attributeRemoved
      %>
      </body>
      </html>
    
  • MyHttpSessionAttributeListener:

      package cn.itcast.servlet.listener;
      
      import javax.servlet.http.HttpSession;
      import javax.servlet.http.HttpSessionAttributeListener;
      import javax.servlet.http.HttpSessionBindingEvent;
      
      public class MyHttpSessionAttributeListener implements
              HttpSessionAttributeListener {
      
          @Override
          public void attributeAdded(HttpSessionBindingEvent se) {
              // 屬性添加
              System.out.println("向session添加了一個屬性...");
              HttpSession session = se.getSession();
      
              System.out.println("屬性名稱:" + se.getName());
              System.out.println("屬性值:" + session.getAttribute(se.getName())); // se.getValue :這個方法不會返回當前屬性(Attribute)的值
          }
      
          @Override
          public void attributeRemoved(HttpSessionBindingEvent se) {
              // 屬性移除
              System.out.println("從session移除了一個屬性....");
      
              System.out.println("屬性名稱:" + se.getName());
      
          }
      
          @Override
          public void attributeReplaced(HttpSessionBindingEvent se) {
              // 屬性替換
              System.out.println("將session中一個屬性值替換為其他值...");
      
              HttpSession session = se.getSession();
              System.out.println("屬性名稱:" + se.getName());
              System.out.println("屬性值:" + session.getAttribute(se.getName()));
          }
      
      }
    

5、監聽綁定到 HttpSession 域中的某個對象的狀態的事件監聽器

  • 保存在 Session 域中的對象可以有多種狀態:

    • 綁定到 Session 中;

    • 從 Session 域中解除綁定;

    • 隨 Session 對象持久化到一個存儲設備中(鈍化);

    • 隨 Session 對象從一個存儲設備中恢復(活化)

  • Servlet 規范中定義了兩個特殊的監聽器接口來幫助 JavaBean 對象了解自己在 Session 域中的這些狀態:HttpSessionBindingListener 接口和 HttpSessionActivationListener 接口,實現這兩個接口的類不需要 web.xml 文件中注冊,因為監聽方法調用,都是由Session自主完成的

    • HttpSessionBindingListener

      實現該接口的 Java 對象,可以感知自己被綁定到 Session 或者從 Session 中解除綁定

    • HttpSessionActivationListener

      實現該接口的 Java 對象,可以感知自己從內存被鈍化硬盤上,或者從硬盤被活化到內存中

5.1、HttpSessionBindingListener接口

  • 實現了 HttpSessionBindingListener 接口的 JavaBean 對象可以感知自己被綁定到 Session 中和從 Session 中刪除的事件

    • 綁定:當對象被綁定到 HttpSession 對象中時,web 服務器調用該 JavaBean 對象的 valueBound 方法

        public void valueBound(HttpSessionBindingEvent event)
      
    • 解綁:當對象從 HttpSession 對象中解除綁定時,web 服務器調用該 JavaBean 對象的 valueUnbound 方法

        public void valueUnbound(HttpSessionBindingEvent event)
      
  • demo:

    • 實現 HttpSessionBindingListener 接口的 JavaBean :

        package cn.itcast.domain;
        
        import javax.servlet.http.HttpSessionBindingEvent;
        import javax.servlet.http.HttpSessionBindingListener;
        
        /**
         * 使Bean1對象感知 自我被綁定Session中,感知自我被Session解除綁定
         * 
         * @author seawind
         * 
         */
        public class Bean1 implements HttpSessionBindingListener {
            private int id;
            private String name;
        
            public int getId() {
                return id;
            }
        
            public void setId(int id) {
                this.id = id;
            }
        
            public String getName() {
                return name;
            }
        
            public void setName(String name) {
                this.name = name;
            }
        
            @Override
            public void valueBound(HttpSessionBindingEvent event) {
                System.out.println("Bean1對象被綁定了...");
                // 當前對象,操作對象
                System.out.println("綁定對象name:" + this.name);
            }
        
            @Override
            public void valueUnbound(HttpSessionBindingEvent event) {
                System.out.println("Bean1對象被解除綁定了...");
                System.out.println("解除綁定對象name:" + this.name);
            }
        
        }
      
    • 新建一個JSP頁面,里面有如下代碼:

        <%@ page language="java" contentType="text/html; charset=UTF-8"
            pageEncoding="UTF-8"%>
        <%@page import="cn.itcast.domain.Bean1"%>
        <!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>Insert title here</title>
        </head>
        <body>
        <%
            Bean1 bean_Susan = new Bean1();
            bean_Susan.setId(100);
            bean_Susan.setName("Susan");
            
            // 將bean_Susan對象以“bean1”為名,綁定到Session中
            session.setAttribute("bean1",bean_Susan);
            
            Bean1 bean_Mary = new Bean1();
            bean_Mary.setId(200);
            bean_Mary.setName("Mary");
      
            // 將bean_Susan對象以“bean1”為名,綁定到Session中
            session.setAttribute("bean1",bean_Mary);
        %>
        ${bean1.name }
        </body>
        </html>
      
    • 此時若開啟服務器,打開這個JSP頁面,控制臺會輸出什么呢?

        Bean1對象被綁定了...
        綁定對象name:bean_Susan
        Bean1對象被綁定了...
        綁定對象name:bean_Mary
        Bean1對象被解除綁定了...
        解除綁定對象name:bean_Susan
      
    • 注意陷阱:當 Session 綁定的 JavaBean 對象替換時,會讓新對象綁定,舊對象解綁

5.2、HttpSessionActivationListener接口

  • 實現了HttpSessionActivationListener接口的 JavaBean 對象可以感知自己被活化和鈍化的事件

    • 當綁定到 HttpSession 對象中的 JavaBean 對象將要隨 HttpSession 對象被鈍化之前,web 服務器調用該 JavaBean 對象的 void sessionWillPassivate(HttpSessionBindingEvent event) 方法

    • 當綁定到 HttpSession 對象中的 JavaBean 對象將要隨 HttpSession 對象被活化之后,web 服務器調用該 JavaBean 對象的 void sessionDidActive(HttpSessionBindingEvent event) 方法

  • 使用場景:Session保存數據,很長一段時間沒用,但是不能銷毀Session對象,又不想占用服務器內存資源 ----- 鈍化(將服務器內存中數據序列化硬盤上)

  • 鈍化和活化應該由 Tomcat 服務器 自動進行,所以應該配置 Tomcat :

      <Context>
          <!-- 1分鐘不用 進行鈍化  表示1分鐘 -->
          <Manager className="org.apache.catalina.session.PersistentManager" maxIdleSwap="1">
              <!-- 鈍化后文件存儲位置  directory="it315" 存放到it315目錄-->
              <Store className="org.apache.catalina.session.FileStore" directory="it315"/>
          </Manager>
      </Context>
    

    配置context有幾個位置?

    1、tomcat/conf/context.xml 對所有虛擬主機 所有web工程生效

    2、tomcat/conf/Catalina/localhost/context.xml 對當前虛擬主機所有web工程生效

    3、當前工程/META-INF/context.xml 對當前工程有效

  • demo:

    • write.jsp:

        <%@ page language="java" contentType="text/html; charset=UTF-8"
            pageEncoding="UTF-8"%>
        <%@page import="cn.itcast.domain.Bean2"%>
        <!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>Insert title here</title>
        </head>
        <body>
        <!-- 將javabean 對象保存Session中 -->
        <%
            Bean2 bean2 = new Bean2();
            bean2.setName("聯想筆記本");
            bean2.setPrice(5000);
            
            session.setAttribute("bean2",bean2);
        %>
        </body>
        </html>
      
    • read.jsp:

        <%@ page language="java" contentType="text/html; charset=UTF-8"
            pageEncoding="UTF-8"%>
        <!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>Insert title here</title>
        </head>
        <body>
        <!-- 讀取javabean對象的數據 -->
        讀取bean2的數據: ${bean2.name } , ${bean2.price }
        </body>
        </html>
      
    • Bean2.java:

        package cn.itcast.domain;
        
        import javax.servlet.http.HttpSessionActivationListener;
        import javax.servlet.http.HttpSessionEvent;
        
        /**
         * 感知鈍化和活化
         * 
         * @author seawind
         * 
         */
        public class Bean2 implements HttpSessionActivationListener {
            private String name;
            private double price;
        
            public String getName() {
                return name;
            }
        
            public void setName(String name) {
                this.name = name;
            }
        
            public double getPrice() {
                return price;
            }
        
            public void setPrice(double price) {
                this.price = price;
            }
        
            @Override
            public void sessionDidActivate(HttpSessionEvent se) {
                System.out.println("bean2對象被活化...");
            }
        
            @Override
            public void sessionWillPassivate(HttpSessionEvent se) {
                System.out.println("bean2對象被鈍化...");
            }
        
        }
      
    • 配置Tomcat后,開啟服務器,打開write.jsp,等待一分鐘,在這一分鐘內,打開read.jsp是可以讀取到bean2對象的。一分鐘過后,read.jsp讀取不到bean2對象了,同時控制臺輸出:

        bean2對象被鈍化...
      
    • 這時候就實現了鈍化的效果。好,接下來按照步驟走,鈍化后it315目錄在哪里? 在項目文件夾是找不到這個目錄的,得去Tomcat服務器的目錄去找:

        tomcat/work/Catalina/localhost/項目工程名/
      
    • 在it315目錄中的確可以看到一個XXXXXX.session的文件,其中XXXXXX就是SessionId,打開這個文件,卻沒有發現 JavaBean 的任何信息,這是為什么呢?

    • 回顧JavaSE的知識,可以發現 JavaBean 沒有序列化。Java對象如果想被序列化,必須實現Serializable接口 ---- Bean2 實現該接口:

        import java.io.Serializable;
        ...
        public class Bean2 implements HttpSessionActivationListener, Serializable {
        ...
        }
      
    • 接下來,重啟服務器,打開write.jsp,一分鐘后,控制臺照舊輸出bean2對象被鈍化的消息,再進去XXXXXX.session文件中,發現可以找到 JavaBean 對象的相關信息(雖然是亂碼)。接下來再打開read.jsp,發現可以讀取到 JavaBean 對象了,并且此時控制臺輸出:

        bean2對象被活化...
      
    • 這就是一種對于Session的優化策略

5.3、案例:在線用戶列表和踢人功能

  • 圖解:

  • demo:

    • MyServletContextListener:

        package cn.itcast.listener;
        
        import java.util.HashMap;
        import java.util.Map;
        
        import javax.servlet.ServletContext;
        import javax.servlet.ServletContextEvent;
        import javax.servlet.ServletContextListener;
        import javax.servlet.http.HttpSession;
        
        import cn.itcast.domain.User;
        
        /**
         * 完成全局數據對象初始化
         * 
         * @author seawind
         * 
         */
        public class MyServletContextListener implements ServletContextListener {
        
            @Override
            public void contextDestroyed(ServletContextEvent sce) {
            }
        
            @Override
            public void contextInitialized(ServletContextEvent sce) {
                // 所有在線用戶數據集合
                Map<User, HttpSession> map = new HashMap<User, HttpSession>();
                // 將集合保存ServletContext 數據范圍
                ServletContext servletContext = sce.getServletContext();
        
                servletContext.setAttribute("map", map);
            }
        
        }
      
    • JavaBean:

        package cn.itcast.domain;
        
        import java.util.Map;
        
        import javax.servlet.ServletContext;
        import javax.servlet.http.HttpSession;
        import javax.servlet.http.HttpSessionBindingEvent;
        import javax.servlet.http.HttpSessionBindingListener;
        
        /**
         * User對象自我感知,綁定Session和解除綁定
         */
        public class User implements HttpSessionBindingListener {
            private int id;
            private String username;
            private String password;
            private String role;
        
            public int getId() {
                return id;
            }
        
            public void setId(int id) {
                this.id = id;
            }
        
            public String getUsername() {
                return username;
            }
        
            public void setUsername(String username) {
                this.username = username;
            }
        
            public String getPassword() {
                return password;
            }
        
            public void setPassword(String password) {
                this.password = password;
            }
        
            public String getRole() {
                return role;
            }
        
            public void setRole(String role) {
                this.role = role;
            }
        
            @Override
            public void valueBound(HttpSessionBindingEvent event) {
                // 將新建立Session 和 用戶 保存ServletContext 的Map中
                HttpSession session = event.getSession();
                ServletContext servletContext = session.getServletContext();
        
                Map<User, HttpSession> map = (Map<User, HttpSession>) servletContext
                        .getAttribute("map");
        
                // 將新用戶加入map
                map.put(this, session);
            }
        
            @Override
            public void valueUnbound(HttpSessionBindingEvent event) {
                // 根據user對象,從Map中移除Session
                HttpSession session = event.getSession();
                ServletContext servletContext = session.getServletContext();
        
                Map<User, HttpSession> map = (Map<User, HttpSession>) servletContext
                        .getAttribute("map");
        
                // 從map移除
                map.remove(this);
            }
        
        }
      
    • LoginServlet:

        package cn.itcast.servlet;
        
        import java.io.IOException;
        import java.sql.SQLException;
        import java.util.Map;
        
        import javax.servlet.ServletException;
        import javax.servlet.http.HttpServlet;
        import javax.servlet.http.HttpServletRequest;
        import javax.servlet.http.HttpServletResponse;
        import javax.servlet.http.HttpSession;
        
        import org.apache.commons.dbutils.QueryRunner;
        import org.apache.commons.dbutils.handlers.BeanHandler;
        
        import cn.itcast.domain.User;
        import cn.itcast.utils.JDBCUtils;
        
        /**
         * 登陸
         * 
         * @author seawind
         * 
         */
        public class LoginServlet extends HttpServlet {
        
            public void doGet(HttpServletRequest request, HttpServletResponse response)
                    throws ServletException, IOException {
                request.setCharacterEncoding("utf-8");
        
                String username = request.getParameter("username");
                String password = request.getParameter("password");
        
                String sql = "select * from user where username = ? and password = ?";
                Object[] args = { username, password };
        
                QueryRunner queryRunner = new QueryRunner(JDBCUtils.getDataSource());
                try {
                    User user = queryRunner.query(sql,
                            new BeanHandler<User>(User.class), args);
                    // 判斷登陸是否成功
                    if (user == null) {
                        // 失敗
                        request.setAttribute("msg", "用戶名或者密碼錯誤!");
                        request.getRequestDispatcher("/login.jsp").forward(request,
                                response);
                    } else {
                        // 成功
                        request.getSession().invalidate();// 銷毀之前狀態
        
                        // 先判斷該用戶是否已經登陸,如果已經登陸,將Session銷毀
                        Map<User, HttpSession> map = (Map<User, HttpSession>) getServletContext()
                                .getAttribute("map");
                        for (User hasLoginUser : map.keySet()) {
                            if (hasLoginUser.getUsername().equals(user.getUsername())) {
                                // 此用戶之前登陸過 --- 消滅Session
                                HttpSession hasLoginSession = map.get(hasLoginUser);
                                hasLoginSession.invalidate();// session 被摧毀,移除所有對象
                                // 若不使用break,則會調用map的remove方法(invalidate -> valueUnbound),發生并發異常
                                break; 
                            }
                        }
        
                        request.getSession().setAttribute("user", user); // 將user對象綁定到Session,觸發valueBound
                        response.sendRedirect("/day18kick/list.jsp");
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        
            public void doPost(HttpServletRequest request, HttpServletResponse response)
                    throws ServletException, IOException {
                doGet(request, response);
            }
        
        }
      
    • KickServlet:

        package cn.itcast.servlet;
        
        import java.io.IOException;
        import java.util.Map;
        
        import javax.servlet.ServletException;
        import javax.servlet.http.HttpServlet;
        import javax.servlet.http.HttpServletRequest;
        import javax.servlet.http.HttpServletResponse;
        import javax.servlet.http.HttpSession;
        
        import cn.itcast.domain.User;
        
        /**
         * 接收被踢id
         * 
         * @author seawind
         * 
         */
        public class KickServlet extends HttpServlet {
        
            public void doGet(HttpServletRequest request, HttpServletResponse response)
                    throws ServletException, IOException {
                String id = request.getParameter("id");// 被踢人 id
        
                Map<User, HttpSession> map = (Map<User, HttpSession>) getServletContext()
                        .getAttribute("map");
                // 查找目標id
                for (User hasLoginUser : map.keySet()) {
                    if (hasLoginUser.getId() == Integer.parseInt(id)) {
                        // 找到被踢用戶記錄
                        HttpSession hasLoginSession = map.get(hasLoginUser);
                        hasLoginSession.invalidate();
                        break;
                    }
                }
                // 跳轉回 列表頁面
                response.sendRedirect("/day18kick/list.jsp");
            }
        
            public void doPost(HttpServletRequest request, HttpServletResponse response)
                    throws ServletException, IOException {
                doGet(request, response);
            }
        
        }
      
    • list.jsp:

        <%@ page language="java" contentType="text/html; charset=UTF-8"
            pageEncoding="UTF-8"%>
        <%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
        <!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>Insert title here</title>
        </head>
        <body>
        <h1>在線用戶列表</h1>
        <h2>當前用戶 ${user.username }</h2>
        <!-- 將ServletContext中 map 數據顯示出來 -->
        <c:forEach items="${map}" var="entry">
            <!-- 只有管理員可以踢人 -->
            <!-- 管理員不能被踢 -->
            ${entry.key.username } 
            <c:if test="${user.role == 'admin' && entry.key.role != 'admin' }">
                <a href="/day18kick/kick?id=${entry.key.id}">踢下線</a> 
            </c:if>
            <br/>
        </c:forEach>
        </body>
        </html>
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 監聽器(listener) 監聽器簡介 :監聽器就是一個實現特定接口的普通java程序,這個程序專門用于監聽另一個...
    奮斗的老王閱讀 2,564評論 0 53
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,765評論 18 399
  • 一. Java基礎部分.................................................
    wy_sure閱讀 3,835評論 0 11
  • 本文包括:1、Filter簡介2、Filter是如何實現攔截的?3、Filter開發入門4、Filter的生命周期...
    廖少少閱讀 7,341評論 3 56
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,366評論 11 349