【Listener】利用监听器Listener以MVC的思想通过JSP+Servlet+JDBC完成在线用户列表的输出

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容器不比放入数据库简单,主要是程序不够清晰。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。