監聽器應用【統計網站人數、自定義session掃描器、踢人小案例】

Java3y發表於2018-02-10

從第一篇已經講解過了監聽器的基本概念,以及Servlet各種的監聽器。這篇博文主要講解的是監聽器的應用。

統計網站線上人數

分析

我們在網站中一般使用Session來標識某使用者是否登陸了,如果登陸了,就在Session域中儲存相對應的屬性。如果沒有登陸,那麼Session的屬性就應該為空。

現在,我們想要統計的是網站的線上人數。我們應該這樣做:我們監聽是否有新的Session建立了,如果新建立了Sesssion,那麼線上人數就應該+1。這個線上人數是整個站點的,所以應該有Context物件儲存。

大致思路:

  • 監聽Session是否被建立了
  • 如果Session被建立了,那麼在Context的域物件的值就應該+1
  • 如果Session從記憶體中移除了,那麼在Context的域物件的值就應該-1.

程式碼

  • 監聽器程式碼:
	public class CountOnline implements HttpSessionListener {
	
	    public void sessionCreated(HttpSessionEvent se) {
	
	        //獲取得到Context物件,使用Context域物件儲存使用者線上的個數
	        ServletContext context = se.getSession().getServletContext();
	        
	        //直接判斷Context物件是否存在這個域,如果存在就人數+1,如果不存在,那麼就將屬性設定到Context域中
	        Integer num = (Integer) context.getAttribute("num");
	        
	        if (num == null) {
	            context.setAttribute("num", 1);
	        } else {
	            num++;
	            context.setAttribute("num", num);
	        }
	    }
	    public void sessionDestroyed(HttpSessionEvent se) {
	
	        ServletContext context = se.getSession().getServletContext();
	        Integer num = (Integer) se.getSession().getAttribute("num");
	
	        if (num == null) {
	            context.setAttribute("num", 1);
	        } else {
	            num--;
	            context.setAttribute("num", num);
	        }
	    }
	}

複製程式碼
  • 顯示頁面程式碼:

線上人數:${num}

複製程式碼

測試

我們每使用一個瀏覽器訪問伺服器,都會新建立一個Session。那麼網站的線上人數就會+1。

使用同一個頁面重新整理,還是使用的是那個Sesssion,所以網站的線上人數是不會變的。

這裡寫圖片描述


自定義Session掃描器

我們都知道Session是儲存在記憶體中的,如果Session過多,伺服器的壓力就會非常大。

但是呢,Session的預設失效時間是30分鐘(30分鐘沒人用才會失效),這造成Seesion可能會過多(沒人用也存在記憶體中,這不是明顯浪費嗎?)

當然啦,我們可以在web.xml檔案中配置Session的生命週期。但是呢,這是由伺服器來做的,我嫌它的時間不夠準確。(有時候我配置了3分鐘,它用4分鐘才幫我移除掉Session)

所以,我決定自己用程式手工移除那些長時間沒人用的Session。

分析

要想移除長時間沒人用的Session,肯定要先拿到全部的Session啦。所以我們使用一個容器來裝載站點所有的Session。。

只要Sesssion一建立了,就把Session新增到容器裡邊。毫無疑問的,我們需要監聽Session了。

接著,我們要做的就是隔一段時間就去掃描一下全部Session,如果有Session長時間沒使用了,我們就把它從記憶體中移除。隔一段時間去做某事,這肯定是定時器的任務呀。

定時器應該在伺服器一啟動的時候,就應該被建立了。因此還需要監聽Context

最後,我們還要考慮到併發的問題,如果有人同時訪問站點,那麼監聽Session建立的方法就會被併發訪問了定時器掃描容器的時候,可能是獲取不到所有的Session的

這需要我們做同步

於是乎,我們已經有大致的思路了

  • 監聽Session和Context的建立
  • 使用一個容器來裝載Session
  • 定時去掃描Session,如果它長時間沒有使用到了,就把該Session從記憶體中移除。
  • 併發訪問的問題

程式碼

  • 監聽器程式碼:

	public class Listener1 implements ServletContextListener,
	        HttpSessionListener {
	
	
	
	    //伺服器一啟動,就應該建立容器。我們使用的是LinkList(涉及到增刪)。容器也應該是執行緒安全的。
	    List<HttpSession> list = Collections.synchronizedList(new LinkedList<HttpSession>());
	
	    //定義一把鎖(Session新增到容器和掃描容器這兩個操作應該同步起來)
	    private Object lock = 1;
	
	    public void contextInitialized(ServletContextEvent sce) {
	
	
	        Timer timer = new Timer();
	        //執行我想要的任務,0秒延時,每10秒執行一次
	        timer.schedule(new MyTask(list, lock), 0, 10 * 1000);
	
	    }
	    public void sessionCreated(HttpSessionEvent se) {
	
	        //只要Session一建立了,就應該新增到容器中
	        synchronized (lock) {
	            list.add(se.getSession());
	        }
	        System.out.println("Session被建立啦");
	
	    }
	
	    public void sessionDestroyed(HttpSessionEvent se) {
	        System.out.println("Session被銷燬啦。");
	    }
	    public void contextDestroyed(ServletContextEvent sce) {
	
	    }
	}

複製程式碼
  • 任務程式碼:



	/*
	* 在任務中應該掃描容器,容器在監聽器上,只能傳遞進來了。
	*
	* 要想得到在監聽器上的鎖,也只能是傳遞進來
	*
	* */
	class MyTask extends TimerTask {
	
	    private List<HttpSession> sessions;
	    private Object lock;
	
	    public MyTask(List<HttpSession> sessions, Object lock) {
	        this.sessions = sessions;
	        this.lock = lock;
	    }
	
	    @Override
	    public void run() {
	
	        synchronized (lock) {
	            //遍歷容器
	            for (HttpSession session : sessions) {
	
	                //只要15秒沒人使用,我就移除它啦
	                if (System.currentTimeMillis() - session.getLastAccessedTime() > (1000 * 15)) {
	                    session.invalidate();
	                    sessions.remove(session);
	                }
	
	            }
	        }
	    }
	}

複製程式碼
  • 測試:

15秒如果Session沒有活躍,那麼就被刪除!

這裡寫圖片描述

  • 使用集合來裝載我們所有的Session
  • 使用定時器來掃描session的宣告週期【由於定時器沒有session,我們傳進去就好了】
  • 關於併發訪問的問題,我們在掃描和檢測session新增的時候,同步起來就好了【當然,定時器的鎖也是要外面傳遞進來的】

踢人小案列

列出所有的線上使用者,後臺管理者擁有踢人的權利,點選踢人的超連結,該使用者就被登出了。

分析

首先,怎麼能列出所有的線上使用者呢??一般我們線上使用者都是用Session來標記的**,所有的線上使用者就應該用一個容器來裝載所有的Session。。**

我們監聽Session的是否有屬性新增(監聽Session的屬性有新增、修改、刪除三個方法。如果監聽到Session新增了,那麼這個肯定是個線上使用者!)。

裝載Session的容器應該是在Context裡邊的【屬於全站點】,並且容器應該使用Map集合【待會還要通過使用者的名字來把使用者踢了】

思路:

  • 寫監聽器,監聽是否有屬性新增在Session裡邊了
  • 寫簡單的登陸頁面。
  • 列出所有的線上使用者
  • 實現踢人功能(也就是摧毀Session)

程式碼

  • 監聽器


public class KickPerson implements HttpSessionAttributeListener {

    // Public constructor is required by servlet spec
    public KickPerson() {
    }

    public void attributeAdded(HttpSessionBindingEvent sbe) {

        //得到context物件,看看context物件是否有容器裝載Session
        ServletContext context = sbe.getSession().getServletContext();

        //如果沒有,就建立一個唄
        Map map = (Map) context.getAttribute("map");
        if (map == null) {
            map = new HashMap();
            context.setAttribute("map", map);
        }

        //---------------------------------------------------------------------------------------
        
        //得到Session屬性的值
        Object o = sbe.getValue();

        //判斷屬性的內容是否是User物件
        if (o instanceof User) {
            User user = (User) o;
            map.put(user.getUsername(), sbe.getSession());
        }
    }

    public void attributeRemoved(HttpSessionBindingEvent sbe) {
      /* This method is called when an attribute
         is removed from a session.
      */
    }

    public void attributeReplaced(HttpSessionBindingEvent sbe) {
      /* This method is invoked when an attibute
         is replaced in a session.
      */
    }
}

複製程式碼
  • 登陸頁面

<form action="${pageContext.request.contextPath }/LoginServlet" method="post">
    使用者名稱:<input type="text" name="username">
    <input type="submit" value="登陸">
</form>


複製程式碼
  • 處理登陸Servlet

        //得到傳遞過來的資料
        String username = request.getParameter("username");

        User user = new User();
        user.setUsername(username);

        //標記該使用者登陸了!
        request.getSession().setAttribute("user", user);

        //提供介面,告訴使用者登陸是否成功
        request.setAttribute("message", "恭喜你,登陸成功了!");
        request.getRequestDispatcher("/message.jsp").forward(request, response);


複製程式碼
  • 列出線上使用者


<c:forEach items="${map}" var="me">

    ${me.key} <a href="${pageContext.request.contextPath}/KickPersonServlet?username=${me.key}">踢了他吧</a>

    <br>
</c:forEach>

複製程式碼
  • 處理踢人的Servlet


        String username = request.getParameter("username");

        //得到裝載所有的Session的容器
        Map map = (Map) this.getServletContext().getAttribute("map");

        //通過名字得到Session
        HttpSession httpSession = (HttpSession) map.get(username);
        httpSession.invalidate();
        map.remove(username);

        //摧毀完Session後,返回列出線上使用者頁面
        request.getRequestDispatcher("/listUser.jsp").forward(request, response);

複製程式碼

測試

使用多個瀏覽器登陸來模擬線上使用者(同一個瀏覽器使用的都是同一個Session)

這裡寫圖片描述


監聽Seesion的建立和監聽Session屬性的變化有啥區別???

  • Session的建立只代表著瀏覽器給伺服器傳送了請求。會話建立
  • Session屬性的變化就不一樣了,登記的是具體使用者是否做了某事(登陸、購買了某商品)

如果文章有錯的地方歡迎指正,大家互相交流。習慣在微信看技術文章的同學,可以關注微信公眾號:Java3y

相關文章