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