2.10.5 使用HttpSessionListener和HttpSessionAttributeListener

2.10.5 使用HttpSessionListener和HttpSessionAttributeListener

HttpSessionListener

HttpSessionListener用于监听用户session的创建和销毁,实现该接口的监听器需要实现如下两个方法。

  • sessionCreated(HttpsessionEvent se):用户与服务器的会话开始、创建时触发该方法
  • sessionDestroyed(HttpsessionEvent se):用户与服务器的会话断开、销毁时触发该方法。

HttpSessionAttributeListener

HttpSessionAttributeListener则用于监听HttpSession(session内置对象)范围内属性的变化,实现该接口的监听器需要实现

  • attributeAdded()
  • attributeRemoved()
  • attributeReplaced()

这三个方法。

由此可见,HttpSessionAttributeListenerServletContextAttributeListener的作用相似,都用于监听属性的改变,只是HttpSessionAttributeListener监听session范围内属性的改变,
ServletContextAttributeListener监听的是application范围内属性的改变。

程序 监听系统在线用户

实现HttpSessionListener接口的监听器可以监听每个用户会话的开始和断开,因此应用可以通过该监听器监听系统的在线用户。

OnlineListener.java

下面是该监听器的实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package lee;

import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;
import java.util.*;

@WebListener
public class OnlineListener implements HttpSessionListener {
// 当用户与服务器之间开始session时触发该方法
public void sessionCreated(HttpSessionEvent se) {
HttpSession session = se.getSession();
ServletContext application = session.getServletContext();
// 获取session ID
String sessionId = session.getId();
// 如果是一次新的会话
if (session.isNew()) {
String user = (String) session.getAttribute("user");
// 未登录用户当游客处理
user = (user == null) ? "游客" : user;
Map<String, String> online = (Map<String, String>) application.getAttribute("online");
if (online == null) {
online = Collections.synchronizedMap(new HashMap<String, String>());
}
// 将用户在线信息放入Map中
online.put(sessionId, user);
application.setAttribute("online", online);
}
}

// 当用户与服务器之间session断开时触发该方法
public void sessionDestroyed(HttpSessionEvent se) {
HttpSession session = se.getSession();
ServletContext application = session.getServletContext();
String sessionId = session.getId();
Map<String, String> online = (Map<String, String>) application.getAttribute("online");
if (online != null) {
// 删除该用户的在线信息
online.remove(sessionId);
}
application.setAttribute("online", online);
}
}

上面的监听器实现类实现了HttpSessionListener接口,该监听器可用于监听用户与服务器之间session的开始、关闭.

  • 当用户与服务器之间的session开始时,如果该session是一次新的session,程序就将当前用户的sessionId、用户名存入application范围的Map中;
  • 当用户与服务器之间的session关闭时,程序从application范围的Map中删除该用户的信息。

通过上面的方式,application范围内的Map就记录了当前应用的所有在线用户。

online.jsp

显示在线用户的页面代码很简单,只要迭代输出application范围的Map即可,如以下代码所

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
<%@ page import="java.util.*" %>
<!DOCTYPE html>
<html>
<head>
<title> 用户在线信息 </title>
</head>
<body>
在线用户:
<table width="400" border="1">
<%
Map<String , String> online = (Map<String , String>)application
.getAttribute("online");
for (String sessionId : online.keySet())
{%>
<tr>
<td><%=sessionId%>
<td><%=online.get(sessionId)%>
</tr>
<%}%>
</body>
</html>

如果在本机启动三个不同的浏览器来模拟三个用户访问该应用,访问online.jsp页面将看到如图2.42所示的页面效果。

程序 监听用户的详细信息

需要指出的是,采用HttpSessionListener监听用户在线信息比较“粗糙”,只能监听到有多少人在线,每个用户的sessionId等基本信息。如果应用需要监听到每个用户停留在哪个页面、本次在线的停留时间、用户的访问IP等信息,则应该考虑定时检查HttpServletRequest来实现。

通过检查HttpservletRequest的做法可以更精确地监控在线用户的状态,这种做法的思路是:

  • 定义一个ServletRequestListener,这个监听器负责监听每个用户请求,当用户请求到达时,系统将用户请求的sessionID、用户名、用户IP、正在访问的资源、访问时间记录下来
  • 启动一条后台线程,这条后台线程每隔一段时间检查上面的每条在线记录,如果某条在线记录的访问时间与当前时间相差超过了指定值,将这条在线记录删除即可。这条后台线程应随着Web应用的启动而启动,可考虑使用ServletContextListener来完成。

RequestListener.java

下面先定义一个ServletRequestListener,它负责监听每次用户请求:
每次用户请求到达时,

  • 如果是新的用户会话,将相关信息插入数据表:
  • 如果是老的用户会话,则更新数据表中已有的在线记录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package lee;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.sql.*;

@WebListener
public class RequestListener implements ServletRequestListener {
// 当用户请求到达、被初始化时触发该方法
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
HttpSession session = request.getSession();
// 获取session ID
String sessionId = session.getId();
// 获取访问的IP和正在访问的页面
String ip = request.getRemoteAddr();
String page = request.getRequestURI();
String user = (String) session.getAttribute("user");
// 未登录用户当游客处理
user = (user == null) ? "游客" : user;
try {
DbDao dd = new DbDao("com.mysql.jdbc.Driver", "jdbc:mysql://localhost:3306/online_inf", "root", "32147");
ResultSet rs = dd.query("select * from online_inf where session_id=?", true, sessionId);
// 如果该用户对应的session ID存在,表明是旧的会话
if (rs.next()) {
// 更新记录
rs.updateString(4, page);
rs.updateLong(5, System.currentTimeMillis());
rs.updateRow();
rs.close();
} else {
// 插入该用户的在线信息
dd.insert("insert into online_inf values(? , ? , ? , ? , ?)", sessionId, user, ip, page,
System.currentTimeMillis());
}
} catch (Exception ex) {
ex.printStackTrace();
}
}

// 当用户请求结束、被销毁时触发该方法
public void requestDestroyed(ServletRequestEvent sre) {
}
}

上面的程序中粗体字代码控制用户会话是新的session,还是已有的session,新的session将插入数据表;旧的session将更新数据表中对应的记录。

接下来定义一个ServletContextListener,它负责启动一条后台线程,这条后台线程将会定期检查在线记录,并删除那些长时间没有重新请求过的记录。

OnlineListener.java

Listener代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package lee;

import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;
import java.sql.*;
import java.awt.event.*;

@WebListener
public class OnlineListener implements ServletContextListener {
// 超过该时间(10分钟)没有访问本站即认为用户已经离线
public final int MAX_MILLIS = 10 * 60 * 1000;

// 应用启动时触发该方法
public void contextInitialized(ServletContextEvent sce) {
// 每5秒检查一次
new javax.swing.Timer(1000 * 5, new ActionListener() {
public void actionPerformed(ActionEvent e) {
try {
DbDao dd = new DbDao("com.mysql.jdbc.Driver", "jdbc:mysql://localhost:3306/online_inf", "root",
"32147");
ResultSet rs = dd.query("select * from online_inf", false);
StringBuffer beRemove = new StringBuffer("(");
while (rs.next()) {
// 如果距离上次访问时间超过了指定时间
if ((System.currentTimeMillis() - rs.getLong(5)) > MAX_MILLIS) {
// 将需要被删除的session ID添加进来
beRemove.append("'");
beRemove.append(rs.getString(1));
beRemove.append("' , ");
}
}
// 有需要删除的记录
if (beRemove.length() > 3) {
beRemove.setLength(beRemove.length() - 3);
beRemove.append(")");
// 删除所有“超过指定时间未重新请求的记录”
dd.modify("delete from online_inf where session_id in " + beRemove.toString());
}
dd.closeConn();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}).start();
}

public void contextDestroyed(ServletContextEvent sce) {
}
}

上面的程序中粗体字代码负责收集系统中“超过指定时间未访问”的在线记录,然后程序通过一条SQL语句删除这些在线记录。
需要指出的是,上面程序启动的后台线程定期检查的时间间隔为5秒,实际项目中这个时间应该适当加大,尤其是在线用户较多时,否则应用将会频繁地检查online_Inf数据表中的全部记录,这将导致系统开销过大

online.jsp

显示在线用户的页面十分简单,只要查询online_Inf表中全部记录,并将这些记录显示出来即可。
以下是该页面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
<%@ page import="java.sql.*,lee.*" %>
<!DOCTYPE html>
<html>
<head>
<title> 用户在线信息 </title>
</head>
<body>
在线用户:
<table width="640" border="1">
<%
DbDao dd = new DbDao("com.mysql.jdbc.Driver"
, "jdbc:mysql://localhost:3306/online_inf"
, "root"
, "32147");
// 查询online_inf表(在线用户表)的全部记录
ResultSet rs = dd.query("select * from online_inf" , false);
while (rs.next())
{%>
<tr>
<td><%=rs.getString(1)%>
<td><%=rs.getString(2)%>
<td><%=rs.getString(3)%>
<td><%=rs.getString(4)%>
</tr>
<%}%>
</body>
</html>

启动不同浏览器访问该应用的不同页面,然后访问online.jsp页面将可看到如图2.43所示的页面效果。

对于应用中所有需要统计在线用户页面,只要将上面的online.jsp页面包含到页面中即可。

data.sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DROP DATABASE IF EXISTS online_inf;

CREATE DATABASE online_inf;

USE online_inf;

CREATE TABLE online_inf
(
session_id VARCHAR(255) PRIMARY KEY,
username VARCHAR(255),
user_ip VARCHAR(255),
access_res VARCHAR(255),
last_access BIGINT
);
;