Servlet,监听器Listener与《【Filter】拦截器Filter》(点击打开链接)是JSP的三大核心组件,实际上监听器Listener相当于数据库里面的触发器,一旦用户触发了某种行为,则可以通过相关的Java文件执行相应的程序。用户在浏览网页的过程中,主要有打开浏览器的动作,对应的行为是Session的创建,可是,用户关闭浏览器的动作,并不是对应Session的消失,因此对于Session的消失我们意义不大;访问任意网页的动作,对应的行为是request请求的创建,request的消失对于我们程序猿来说没有任何意义;服务器的自身启动与关闭。对应的行为是Application的创建与消失。
利用监听器Listener配合数据库,可以完成在线用户列表的统计。
一、基本目标
输出一个在线用户列表,设定用户访问我们的网站127.0.0.1:8080/Listener则认为其在线,其实就是localhost:8080/Listener,但localhost:8080,IP地址则变成了0::0:1一个IP6地址非常难看,所以还是使用127.0.0.1:8080,由于无法监听用户是否关闭浏览器,因此设定要是用户5秒内没有访问我们网站的任意一个网页,则认为其已经离线了,只是为了看到实验效果,应该设定得更长。
如下图,开两个浏览器,每一个浏览器对应一个Session,认为是两个用户在访问我们的网站。其实你利用监听器,还可以做得复杂点,通过检查此用户名是否登陆的方式来判断其是否登陆。正如此前我在《【php】基于Xajax的在线聊天室、直播间》(点击打开链接)做过的那样。
二、基本准备
首先在数据库中建立一张在线用户表,如下图:
这张表没有主键,因为需要多次被insert与delete擦写,我也不打算通过主键来统计历史在线人数了,免得主键太难看,所以不设置主键。
由于不设置主键,所以不能通过图形化建表,如果你是通过MySQLQueryBrowser去建表的话,而不是MySQL Command Line Client的话,应该如下图:
在查询语句输入框输入:
create table onlineTable(
sessionId varchar(45),
ip varchar(45),
timeonline LONG
)
建好表之后,在eclipse新建一个网络工程ListenerTest,把上次《【Servlet】根据MVC思想设计用户登陆、用户注册、修改密码系统》( 点击打开链接)的Servlet与JDBC的包放到lib,这两个lib网上一搜一把,同时把dbDAO.java放到ListenerTest的src文件夹,并在里面新增一条与插入、修改完全一模一样的删除delete方法,最后整个dbDAO.java如下,几乎就是完全一模一样的,什么都没有改,这就是MVC的优势,由于我们用到同样的一个数据库test,疼一次写好数据库增删改查的类,以后做到多次复用就幸福了。
import java.sql.*;
public class dbDAO {
private Connection con;
// 构造函数,连接数据库
public dbDAO() throws Exception {
String dburl = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&useOldAliasMetadataBehavior=true";
String dbusername = "root";
String dbpassword = "root";
Class.forName("com.mysql.jdbc.Driver");
this.con = DriverManager.getConnection(dburl, dbusername, dbpassword);
}
// 执行查询
public ResultSet query(String sql, Object... args) throws Exception {
PreparedStatement ps = con.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
return ps.executeQuery();
}
// 执行插入
public boolean insert(String sql, Object... args) throws Exception {
PreparedStatement ps = con.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
if (ps.executeUpdate() != 1) {
return false;
}
return true;
}
// 执行修改
public boolean modify(String sql, Object... args) throws Exception {
PreparedStatement ps = con.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
if (ps.executeUpdate() != 1) {
return false;
}
return true;
}
// 执行删除
public boolean delete(String sql, Object... args) throws Exception {
PreparedStatement ps = con.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
if (ps.executeUpdate() != 1) {
return false;
}
return true;
}
// 析构函数,中断数据库的连接
protected void finalize() throws Exception {
if (!con.isClosed() || con != null) {
con.close();
}
}
}
之后配置好web.xml,这样的片段一般和过滤器一样放置到最顶端,表示整个网站的行为由根目录的onlineListener.java监听,写上这样的监听代码,之后用户一旦触发某种行为,如果在onlineListener.java有相应的代码,则这些代码则会被执行:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<listener>
<listener-class>onlineListener</listener-class>
</listener>
</web-app>
最后整体的网络结构如下图:
三、制作过程
1、其实就是写好一个Listener.java就OK。与《【Filter】拦截器Filter》(点击打开链接)中一样,一旦我们要监听某一个动作,就必须重写下这个动作的销毁与创建实现方法,因为这里用到了接口,你不写还真的不行。同时也不要怕一个方法太长记不住,Eclipse for JavaEE会帮你自动生成的。可以被监听的方法有ServletRequestListener表示用户访问任意个网址,每访问一个网页则监听/触发一次,实际上就是监听request对象;ServletContextListener服务器的开始与结束监听/触发一次,实际上就是监听Application对象,通过对Application对象的监听可以达到《【Servlet】利用load-on-startup创造一条随服务器共存亡的线程》(点击打开链接)的效果;还有HttpSessionListener,在用户打开浏览器监听/触发一次,实际上监听Session对象的创建与销毁,这里没有用到。
import java.util.*;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class onlineListener implements ServletRequestListener,
ServletContextListener {
// request对象的销毁对我们意义不大
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
}
// request对象的创建相当于,用户访问任意个网页
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
// 这个方法的参数可以转化成request对象
HttpServletRequest request = (HttpServletRequest) servletRequestEvent
.getServletRequest();
// request对象中取session的方法
HttpSession session = request.getSession();
String sessionId = session.getId();
// request对象中取ip的方法
String ip = request.getRemoteAddr();
// 数据库的查询结果
ResultSet rs = null;
try {
// 如果这个sessionID已经在在线用户列表里面的
// 用户是在线的
// 那么更新其在线时间
dbDAO db = new dbDAO();
rs = db.query("select * from onlinetable where sessionId=?",
sessionId);
if (rs.next()) {
db.modify(
"update onlinetable set timeonline=? where sessionId=?",
System.currentTimeMillis(), sessionId);
}
// 否则插入在线用户列表
else {
db.insert("insert into onlinetable values(?,?,?)", sessionId,
ip, System.currentTimeMillis());
}
// 把当前的在线用户列表放到application里面
rs = db.query("select * from onlinetable");
} catch (Exception e) {
e.printStackTrace();
}
// session.getServletContext()相当于application,用application存放在线用户列表
session.getServletContext().setAttribute("onlineTable", rs);
}
// application的消失对我们的意义不大
// 相当于服务器的关闭,一切都消失了
@Override
public void contextDestroyed(ServletContextEvent arg0) {
// TODO Auto-generated method stub
}
// application的开始相当于服务器的启动
@Override
public void contextInitialized(ServletContextEvent arg0) {
// TODO Auto-generated method stub
// 服务器一旦启动每5秒执行如下的任务
Timer timer = new Timer();
timer.schedule(new MyTask(), 0, 5000);
}
}
class MyTask extends TimerTask {
public void run() {
try {
// 对在线用户列表进行检查
dbDAO db = new dbDAO();
ResultSet rs = db.query("select * from onlinetable");
while (rs.next()) {
// 如果当前时间距离用户上一次在线时间超过5秒
// 那么则从在线用户列表删除这个结果。
if (System.currentTimeMillis() - rs.getLong("timeonline") > 5 * 1000) {
db.delete("delete from onlinetable where sessionId=?",
rs.getString("sessionId"));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里,用到了《【Java】有关System.currentTimeMillis()的思考》( 点击打开链接),取出1970年1月1日到现在的毫秒数的概念生成时间戳,与《【Java】利用Timer与TimerTask定时执行任务》( 点击打开链接)的概念,每5秒执行一次任务。
2、之后编写一个online.jsp用来显示当前数据库的在线用户列表,因为监听器在每一次监听request请求的过程中,已经把在线用户列表放进application容器里面,并且不断更新里面的消息。application容器是一个所有用户都能看到的,服务器上面的大容器。区别于session容器,是用户每次打开浏览器之后,只是这个浏览器所能够看到的小容器。online.jsp读取这个application容器中的在线用户列表查询结果就可以了。注意取出来的对象,要经过强制类型转换,不转换被报错。
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%@ page import="java.sql.*"%>
<!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>在线用户列表</title>
</head>
<body>
在线用户列表
<%ResultSet rs=(ResultSet)application.getAttribute("onlineTable"); %>
<table border="1">
<tr>
<td>ip</td>
<td>sessionId</td>
</tr>
<%
while(rs.next()){
%>
<tr>
<td><%=rs.getString(2)%></td>
<td><%=rs.getString(1)%></td>
</tr>
<%} %>
</table>
</body>
</html>
四、总结与展望
其实,整个网络工程的MVC分层如下,MODEL还是之前《【Servlet】根据MVC思想设计用户登陆、用户注册、修改密码系统》(点击打开链接)写好的MODEL,这里由于是同一数据库,完全可以哪里注意。online.jsp作为view,不直接查询数据库,读出C层监听器,放入的查询结果。
虽然利用到《【Java】用JDK1.5之后的新型数组遍历方法遍历HashMap、HashMap不应该存储多元组》(点击打开链接)提到的多元组,同样可以存放在线用户信息,但是之所以使用到数据库存放在线用户信息,是因为可以避免设置一个存放类的ArrayList放入Application容器。存放类的ArrayList放入Application容器不比放入数据库简单,主要是程序不够清晰。