天天看點

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

從第一篇已經講解過了監聽器的基本概念,以及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

更多的文章可往:文章的目錄導航