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