在前二十二天的学习中,我们已经掌握了 JavaWeb 开发的核心技术栈,包括 Servlet、JSP、会话管理、过滤器、AJAX、分页排序等。今天我们将深入学习 JavaWeb 三大组件的最后一个 ------监听器(Listener)。
监听器是一种特殊的组件,它能够监听 Web 应用中特定事件的发生(如应用启动、会话创建、属性变化等),并在事件发生时执行相应的处理逻辑。监听器在系统初始化、资源管理、状态监控等方面有着不可替代的作用,是构建响应式 Web 应用的重要工具。
监听器概述
什么是监听器
监听器(Listener) 是 JavaWeb 中用于监听 Web 应用内部事件并作出响应的组件。它基于观察者模式设计,包含两个核心角色:
- 事件源:产生事件的对象(如 ServletContext、HttpSession、ServletRequest)
- 监听器:监听事件源产生的事件,并在事件发生时执行特定操作
监听器的工作流程:
- 监听器注册到事件源,声明要监听的事件
- 当事件源发生特定事件时,自动通知所有注册的监听器
- 监听器接收到事件通知后,执行预设的处理逻辑
监听器的作用
监听器主要用于解决以下问题:
- 监控 Web 应用的生命周期(启动、关闭)
- 跟踪用户会话的创建与销毁
- 监听请求的产生与结束
- 监测域对象(ServletContext、HttpSession、ServletRequest)中属性的变化
- 实现组件间的解耦通信(基于事件机制)
- 资源的初始化与释放(如数据库连接池、缓存)
监听器的优势
- 事件驱动:基于事件响应模式,无需主动调用,自动触发
- 解耦设计:监听器与事件源分离,降低组件间耦合度
- 全局监控:可对整个应用、所有会话或请求进行统一监控
- 生命周期管理:精确控制资源在合适的时机初始化和释放
- 灵活性:可根据需要注册或移除监听器,动态调整系统行为
监听器核心接口
JavaWeb 规范定义了 8 种监听器接口,分为三大类:生命周期监听器 、属性监听器 和对象感知监听器。
1. 生命周期监听器
用于监听域对象(ServletContext、HttpSession、ServletRequest)的创建与销毁事件。
监听器接口 | 事件源 | 主要方法 | 作用 |
---|---|---|---|
ServletContextListener |
ServletContext | contextInitialized() contextDestroyed() |
监听 Web 应用的启动与关闭 |
HttpSessionListener |
HttpSession | sessionCreated() sessionDestroyed() |
监听用户会话的创建与销毁 |
ServletRequestListener |
ServletRequest | requestInitialized() requestDestroyed() |
监听请求的创建与销毁 |
2. 属性监听器
用于监听域对象中属性的添加、移除和替换事件。
监听器接口 | 事件源 | 主要方法 | 作用 |
---|---|---|---|
ServletContextAttributeListener |
ServletContext | attributeAdded() attributeRemoved() attributeReplaced() |
监听应用域属性变化 |
HttpSessionAttributeListener |
HttpSession | attributeAdded() attributeRemoved() attributeReplaced() |
监听会话域属性变化 |
ServletRequestAttributeListener |
ServletRequest | attributeAdded() attributeRemoved() attributeReplaced() |
监听请求域属性变化 |
3. 会话对象感知监听器
用于监听会话中特定对象的绑定与解绑事件(无需注册,由对象自身实现)。
监听器接口 | 实现者 | 主要方法 | 作用 |
---|---|---|---|
HttpSessionBindingListener |
会话中的对象 | valueBound() valueUnbound() |
感知对象与会话的绑定 / 解绑 |
HttpSessionActivationListener |
会话中的对象 | sessionWillPassivate() sessionDidActivate() |
感知会话的钝化 / 活化 |
监听器实现与配置
1. 实现监听器的步骤
实现一个监听器通常需要以下步骤:
- 编写类实现相应的监听器接口
- 重写接口中的事件处理方法
- 注册监听器(通过注解或 XML 配置)
2. 注册监听器的方式
(1)注解配置
使用@WebListener
注解注册监听器:
@WebListener
public class MyServletContextListener implements ServletContextListener {
// 实现接口方法...
}
(2)XML 配置(web.xml)
在 web.xml 中配置监听器:
<listener>
<listener-class>com.example.listener.MyServletContextListener</listener-class>
</listener>
注意:监听器的配置顺序不影响其执行顺序,执行顺序由事件发生的时机决定。
常用监听器应用场景
1. ServletContextListener:应用生命周期管理
ServletContextListener
用于监听 Web 应用的启动(初始化)和关闭(销毁)事件,适合执行全局初始化和资源释放操作。
示例 1:应用初始化与资源释放
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* 应用上下文监听器
* 负责应用启动时的初始化和关闭时的资源释放
*/
@WebListener
public class AppContextListener implements ServletContextListener {
// 应用启动时触发
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("===== Web应用启动 =====");
ServletContext context = sce.getServletContext();
try {
// 1. 加载应用配置文件
Properties config = new Properties();
InputStream in = context.getResourceAsStream("/WEB-INF/config.properties");
config.load(in);
// 将配置存入ServletContext,供整个应用使用
context.setAttribute("appConfig", config);
System.out.println("配置文件加载成功,应用名称:" + config.getProperty("app.name"));
// 2. 初始化数据库连接池
// DataSource dataSource = initDataSource(config);
// context.setAttribute("dataSource", dataSource);
// System.out.println("数据库连接池初始化成功");
// 3. 记录应用启动时间
context.setAttribute("startupTime", System.currentTimeMillis());
System.out.println("应用初始化完成");
} catch (IOException e) {
System.err.println("应用初始化失败:" + e.getMessage());
// 严重错误时可以抛出异常,阻止应用启动
throw new RuntimeException("配置文件加载失败", e);
}
}
// 应用关闭时触发
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("===== Web应用关闭 =====");
ServletContext context = sce.getServletContext();
// 1. 释放数据库连接池
// DataSource dataSource = (DataSource) context.getAttribute("dataSource");
// if (dataSource instanceof HikariDataSource) {
// ((HikariDataSource) dataSource).close();
// System.out.println("数据库连接池已关闭");
// }
// 2. 记录应用运行时间
Long startupTime = (Long) context.getAttribute("startupTime");
if (startupTime != null) {
long runTime = (System.currentTimeMillis() - startupTime) / 1000;
System.out.println("应用运行时间:" + runTime + "秒");
}
System.out.println("应用资源释放完成");
}
}
应用场景:
- 加载全局配置文件(数据库连接参数、系统参数等)
- 初始化数据库连接池、缓存等资源
- 注册全局服务或组件
- 记录应用启动时间和运行时长
- 应用关闭时释放资源,防止内存泄漏
2. HttpSessionListener:会话管理与在线用户统计
HttpSessionListener
用于监听用户会话的创建和销毁,适合实现在线用户统计、会话超时管理等功能。
示例 2:在线用户统计
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* 会话监听器
* 实现在线用户统计功能
*/
@WebListener
public class OnlineUserListener implements HttpSessionListener {
// 存储所有在线用户的会话ID(线程安全)
private static Set<String> onlineUsers = Collections.synchronizedSet(new HashSet<>());
// 会话创建时触发(用户首次访问,会话创建)
@Override
public void sessionCreated(HttpSessionEvent se) {
HttpSession session = se.getSession();
// 将新会话ID添加到在线用户集合
onlineUsers.add(session.getId());
// 更新在线人数到应用上下文
session.getServletContext().setAttribute("onlineUserCount", onlineUsers.size());
System.out.println("用户会话创建:" + session.getId() +
",当前在线人数:" + onlineUsers.size());
}
// 会话销毁时触发(会话超时或手动销毁)
@Override
public void sessionDestroyed(HttpSessionEvent se) {
HttpSession session = se.getSession();
// 从在线用户集合中移除会话ID
onlineUsers.remove(session.getId());
// 更新在线人数到应用上下文
session.getServletContext().setAttribute("onlineUserCount", onlineUsers.size());
System.out.println("用户会话销毁:" + session.getId() +
",当前在线人数:" + onlineUsers.size());
}
// 获取在线用户数量
public static int getOnlineUserCount() {
return onlineUsers.size();
}
// 获取所有在线用户会话ID
public static Set<String> getOnlineUsers() {
return new HashSet<>(onlineUsers); // 返回副本,防止外部修改
}
}
配合 JSP 页面显示在线人数:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>在线用户统计</title>
</head>
<body>
<h1>当前在线人数:${onlineUserCount}</h1>
<p>刷新页面不会改变在线状态,关闭浏览器一段时间后会话超时会减少在线人数</p>
</body>
</html>
扩展功能:
- 结合
HttpSessionAttributeListener
监听用户登录状态,统计真实登录用户 - 实现强制用户下线功能(通过会话 ID 找到会话并调用
invalidate()
) - 记录用户会话的持续时间和活跃情况
- 将会话信息存储到数据库,实现分布式环境下的在线用户统计
3. ServletRequestListener:请求监控与性能分析
ServletRequestListener
用于监听请求的创建和销毁,可用于记录请求信息、分析请求性能等。
示例 3:请求性能监控
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 请求监听器
* 监控请求处理时间和性能
*/
@WebListener
public class RequestPerformanceListener implements ServletRequestListener {
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
// 请求创建时触发
@Override
public void requestInitialized(ServletRequestEvent sre) {
ServletRequest request = sre.getServletRequest();
// 记录请求开始时间
long startTime = System.currentTimeMillis();
request.setAttribute("requestStartTime", startTime);
if (request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
System.out.println("[" + sdf.format(new Date(startTime)) + "] " +
"请求开始: " + httpRequest.getMethod() + " " +
httpRequest.getRequestURI());
}
}
// 请求销毁时触发
@Override
public void requestDestroyed(ServletRequestEvent sre) {
ServletRequest request = sre.getServletRequest();
// 获取请求开始时间
Long startTime = (Long) request.getAttribute("requestStartTime");
if (startTime != null && request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 计算请求处理时间
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
System.out.println("[" + sdf.format(new Date(endTime)) + "] " +
"请求结束: " + httpRequest.getMethod() + " " +
httpRequest.getRequestURI() +
",耗时: " + costTime + "ms");
// 记录慢请求(处理时间超过500ms)
if (costTime > 500) {
System.err.println("警告:慢请求 - " +
httpRequest.getRequestURI() +
",耗时: " + costTime + "ms");
// 可以将慢请求信息写入日志或数据库
}
}
}
}
应用场景:
- 统计请求处理时间,识别慢请求
- 记录请求 URL、方法、客户端 IP 等信息
- 实现请求跟踪(如生成唯一请求 ID,贯穿整个请求处理过程)
- 监控系统负载和性能瓶颈
- 实现请求级别的资源隔离
4. 属性监听器:域属性变化监控
属性监听器用于监听域对象中属性的添加、移除和替换事件,可用于跟踪数据变化、实现数据同步等。
示例 4:会话属性变化监控
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
/**
* 会话属性监听器
* 监控会话中属性的变化,特别是用户登录状态
*/
@WebListener
public class SessionAttributeListener implements HttpSessionAttributeListener {
// 向会话添加属性时触发
@Override
public void attributeAdded(HttpSessionBindingEvent se) {
String attributeName = se.getName();
Object value = se.getValue();
// 监控用户登录(假设用户登录后会将用户对象存入会话,属性名为"user")
if ("user".equals(attributeName)) {
User user = (User) value;
System.out.println("用户登录:" + user.getUsername() +
",会话ID:" + se.getSession().getId());
// 可以在这里记录登录日志、更新用户最后登录时间等
}
System.out.println("会话属性添加:" + attributeName + " = " + value);
}
// 从会话移除属性时触发
@Override
public void attributeRemoved(HttpSessionBindingEvent se) {
String attributeName = se.getName();
Object value = se.getValue();
// 监控用户登出
if ("user".equals(attributeName)) {
User user = (User) value;
System.out.println("用户登出:" + user.getUsername() +
",会话ID:" + se.getSession().getId());
}
System.out.println("会话属性移除:" + attributeName + " = " + value);
}
// 会话中属性被替换时触发
@Override
public void attributeReplaced(HttpSessionBindingEvent se) {
String attributeName = se.getName();
Object oldValue = se.getValue();
Object newValue = se.getSession().getAttribute(attributeName);
System.out.println("会话属性替换:" + attributeName +
",旧值:" + oldValue +
",新值:" + newValue);
}
}
应用场景:
- 监控用户登录 / 登出状态变化
- 跟踪关键数据的修改记录
- 实现数据缓存的同步更新
- 验证属性值的合法性
- 记录属性变化的审计日志
5. HttpSessionBindingListener:对象会话感知
HttpSessionBindingListener
是一种特殊的监听器,由会话中的对象自身实现,无需在 web.xml 或注解中注册,当对象被绑定到会话或从会话中移除时自动触发。
示例 5:用户对象会话感知
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import java.util.Date;
/**
* 用户实体类
* 实现HttpSessionBindingListener,感知与会话的绑定/解绑
*/
public class User implements HttpSessionBindingListener {
private Integer id;
private String username;
private String email;
private Date lastLoginTime;
// 对象被绑定到会话时触发
@Override
public void valueBound(HttpSessionBindingEvent event) {
System.out.println("用户[" + username + "]被绑定到会话:" + event.getSession().getId());
// 可以在这里更新用户状态,如设置登录时间
this.lastLoginTime = new Date();
}
// 对象从会话中解绑时触发
@Override
public void valueUnbound(HttpSessionBindingEvent event) {
System.out.println("用户[" + username + "]从会话解绑:" + event.getSession().getId());
// 可以在这里执行清理操作,如记录登出时间
}
// getter和setter方法
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Date getLastLoginTime() {
return lastLoginTime;
}
}
使用场景:
- 用户登录 / 登出时自动执行相关操作
- 临时数据与会话绑定 / 解绑时的资源管理
- 跟踪对象在会话中的生命周期
- 实现会话级别的数据缓存与失效处理
6. HttpSessionActivationListener:会话钝化与活化
HttpSessionActivationListener
用于监听会话的钝化(序列化到磁盘)和活化(从磁盘反序列化)事件,通常用于分布式环境或会话持久化场景。
示例 6:会话活化与钝化处理
import javax.servlet.http.HttpSessionActivationListener;
import javax.servlet.http.HttpSessionEvent;
import java.io.Serializable;
import java.util.Date;
/**
* 购物车类
* 实现HttpSessionActivationListener和Serializable,支持会话钝化/活化
*/
public class ShoppingCart implements HttpSessionActivationListener, Serializable {
private static final long serialVersionUID = 1L;
private Integer userId;
private String sessionId;
private Date createTime;
// 其他购物车属性...
public ShoppingCart() {
this.createTime = new Date();
}
// 会话即将被钝化(序列化到磁盘)时触发
@Override
public void sessionWillPassivate(HttpSessionEvent se) {
this.sessionId = se.getSession().getId();
System.out.println("购物车[" + userId + "]将被钝化,会话ID:" + sessionId);
// 可以在这里释放不需要序列化的资源
// 如关闭临时数据库连接、清理大对象等
}
// 会话已被活化(从磁盘反序列化)时触发
@Override
public void sessionDidActivate(HttpSessionEvent se) {
System.out.println("购物车[" + userId + "]已被活化,会话ID:" + sessionId);
// 可以在这里重新初始化资源
// 如重新获取数据库连接、刷新缓存数据等
}
// getter和setter方法...
}
会话钝化 / 活化配置(在 META-INF/context.xml 中):
<Context>
<!-- 配置会话钝化相关参数 -->
<Manager className="org.apache.catalina.session.PersistentManager" maxIdleSwap="1">
<!-- 会话空闲1分钟后钝化到磁盘 -->
<Store className="org.apache.catalina.session.FileStore" directory="${catalina.base}/temp/sessions"/>
</Manager>
</Context>
应用场景:
- 分布式系统中的会话共享
- 服务器内存不足时将会话持久化到磁盘
- 长时间未活动的会话资源释放
- 确保会话中的对象在序列化 / 反序列化过程中正确处理资源
监听器执行顺序与事件传播
1. 监听器执行顺序
当多个监听器监听同一类型的事件时,执行顺序遵循以下规则:
- 对于相同类型的监听器,XML 配置的监听器优先于注解配置的监听器
- XML 中配置的监听器,按
<listener>
元素出现的顺序执行 - 注解配置的监听器,执行顺序由容器决定(通常按类名排序,不推荐依赖)
2. 事件传播特点
- 监听器对事件的处理不会影响事件源的默认行为(如会话销毁事件,即使监听器报错,会话仍然会被销毁)
- 监听器之间相互独立,一个监听器的异常不会影响其他监听器的执行
- 监听器方法执行在与事件源相同的线程中,长时间操作会阻塞事件源
监听器最佳实践
1. 监听器设计原则
- 单一职责:一个监听器只处理一类事件或相关功能
- 轻量化:监听器中的处理逻辑应简洁高效,避免耗时操作
- 线程安全:多个线程可能同时触发事件,需确保监听器线程安全
- 资源管理:在监听器中创建的资源必须在适当的时机释放
- 异常处理:监听器中必须捕获异常,避免影响事件源
2. 常见问题及解决方案
(1)监听器不生效问题
- 检查监听器是否正确实现了相应的接口
- 确认监听器已通过注解或 XML 正确注册
- 验证事件是否确实发生(如会话销毁需要等待超时或手动调用
invalidate()
) - 检查监听器类是否在类路径下,是否有正确的包名
(2)性能问题
- 避免在监听器中执行耗时操作(如数据库查询、网络请求)
- 对于复杂处理,可使用异步线程(但需注意线程安全)
- 合理选择监听器类型,避免过度监听(如不需要时不监听所有请求)
(3)内存泄漏问题
- 在
contextDestroyed()
中确保释放所有全局资源 - 会话监听器中避免持有会话的强引用
- 注意监听器与其他组件的循环引用
(4)分布式环境问题
- 标准监听器在分布式环境中只对本地 JVM 有效
- 会话相关监听器需要配合分布式会话实现(如 Redis 共享会话)
- 跨节点的事件通知需要额外的消息机制(如 JMS、RabbitMQ)
3. 监听器与过滤器的配合使用
监听器和过滤器经常配合使用,形成完整的处理链:
- 初始化阶段 :
ServletContextListener
初始化全局资源 - 请求阶段 :
ServletRequestListener
记录请求开始时间- 过滤器链处理请求(编码、权限、安全等)
- 目标资源处理请求
- 响应阶段 :
- 过滤器链处理响应
ServletRequestListener
记录请求结束时间,计算处理耗时
- 会话管理 :
HttpSessionListener
跟踪用户会话- 属性监听器监控会话数据变化
- 销毁阶段 :
ServletContextListener
释放全局资源
综合案例:网站访问统计系统
使用多种监听器实现一个完整的网站访问统计系统,包含以下功能:
- 统计网站总访问量
- 统计当前在线人数
- 记录今日访问量
- 统计各页面访问次数
- 记录系统运行时间
1. 统计数据实体类
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 网站统计数据类
*/
public class StatisticsData {
private long totalVisits; // 总访问量
private int onlineUsers; // 当前在线人数
private long todayVisits; // 今日访问量
private Date lastResetTime; // 今日统计重置时间
private Map<String, Long> pageVisits; // 各页面访问次数
public StatisticsData() {
this.totalVisits = 0;
this.onlineUsers = 0;
this.todayVisits = 0;
this.lastResetTime = new Date();
this.pageVisits = new HashMap<>();
}
// getter和setter方法...
}
2. 统计监听器实现
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestEvent;
import javax.servlet.http.HttpServletRequestListener;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
/**
* 网站访问统计监听器
* 整合多种监听器功能,实现全面的访问统计
*/
@WebListener
public class StatisticsListener implements
ServletContextListener, HttpSessionListener, HttpServletRequestListener {
private ServletContext context;
// 应用启动时初始化统计数据
@Override
public void contextInitialized(ServletContextEvent sce) {
this.context = sce.getServletContext();
// 创建统计数据对象并存储到应用上下文
StatisticsData stats = new StatisticsData();
context.setAttribute("statisticsData", stats);
System.out.println("统计系统初始化完成");
}
// 应用关闭时保存统计数据
@Override
public void contextDestroyed(ServletContextEvent sce) {
StatisticsData stats = (StatisticsData) context.getAttribute("statisticsData");
if (stats != null) {
System.out.println("===== 网站统计数据 =====");
System.out.println("总访问量:" + stats.getTotalVisits());
System.out.println("今日访问量:" + stats.getTodayVisits());
System.out.println("最后重置时间:" + stats.getLastResetTime());
System.out.println("======================");
// 实际应用中可以将统计数据保存到数据库
}
}
// 会话创建时增加在线人数
@Override
public void sessionCreated(HttpSessionEvent se) {
StatisticsData stats = (StatisticsData) context.getAttribute("statisticsData");
if (stats != null) {
stats.setOnlineUsers(stats.getOnlineUsers() + 1);
}
}
// 会话销毁时减少在线人数
@Override
public void sessionDestroyed(HttpSessionEvent se) {
StatisticsData stats = (StatisticsData) context.getAttribute("statisticsData");
if (stats != null && stats.getOnlineUsers() > 0) {
stats.setOnlineUsers(stats.getOnlineUsers() - 1);
}
}
// 请求创建时更新访问统计
@Override
public void requestInitialized(HttpServletRequestEvent sre) {
StatisticsData stats = (StatisticsData) context.getAttribute("statisticsData");
if (stats == null) return;
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
// 1. 总访问量+1
stats.setTotalVisits(stats.getTotalVisits() + 1);
// 2. 检查是否需要重置今日统计(跨天)
checkAndResetTodayStats(stats);
// 3. 今日访问量+1
stats.setTodayVisits(stats.getTodayVisits() + 1);
// 4. 页面访问次数统计
String uri = request.getRequestURI();
Map<String, Long> pageVisits = stats.getPageVisits();
pageVisits.put(uri, pageVisits.getOrDefault(uri, 0L) + 1);
}
@Override
public void requestDestroyed(HttpServletRequestEvent sre) {
// 不需要处理
}
// 检查是否跨天,需要重置今日统计
private void checkAndResetTodayStats(StatisticsData stats) {
Date now = new Date();
Calendar lastResetCal = Calendar.getInstance();
lastResetCal.setTime(stats.getLastResetTime());
Calendar nowCal = Calendar.getInstance();
nowCal.setTime(now);
// 如果不是同一天,则重置今日统计
if (lastResetCal.get(Calendar.YEAR) != nowCal.get(Calendar.YEAR) ||
lastResetCal.get(Calendar.DAY_OF_YEAR) != nowCal.get(Calendar.DAY_OF_YEAR)) {
stats.setTodayVisits(0);
stats.setLastResetTime(now);
System.out.println("已重置今日访问统计");
}
}
}
3. 统计数据展示页面
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html>
<html>
<head>
<title>网站访问统计</title>
<style>
.container { width: 800px; margin: 50px auto; }
.stats-box { border: 1px solid #ccc; padding: 20px; margin-bottom: 20px; }
.stats-item { margin: 10px 0; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #ccc; padding: 10px; text-align: left; }
th { background-color: #f5f5f5; }
</style>
</head>
<body>
<div class="container">
<h1>网站访问统计</h1>
<div class="stats-box">
<div class="stats-item">
<strong>总访问量:</strong> ${statisticsData.totalVisits}
</div>
<div class="stats-item">
<strong>当前在线人数:</strong> ${statisticsData.onlineUsers}
</div>
<div class="stats-item">
<strong>今日访问量:</strong> ${statisticsData.todayVisits}
(<fmt:formatDate value="${statisticsData.lastResetTime}" pattern="yyyy-MM-dd HH:mm:ss"/> 重置)
</div>
</div>
<h2>页面访问统计</h2>
<table>
<tr>
<th>页面URL</th>
<th>访问次数</th>
</tr>
<c:forEach var="entry" items="${statisticsData.pageVisits}">
<tr>
<td>${entry.key}</td>
<td>${entry.value}</td>
</tr>
</c:forEach>
</table>
</div>
</body>
</html>
总结与实践
知识点回顾
-
监听器基础:
- 监听器用于监听 Web 应用中的事件,基于观察者模式
- 三大类监听器:生命周期监听器、属性监听器、对象感知监听器
- 核心接口:
ServletContextListener
、HttpSessionListener
、ServletRequestListener
等
-
监听器应用:
ServletContextListener
:应用初始化、资源管理HttpSessionListener
:在线用户统计、会话管理ServletRequestListener
:请求监控、性能分析- 属性监听器:跟踪域对象属性变化
- 会话对象感知监听器:对象与会话的绑定 / 解绑、钝化 / 活化
-
最佳实践:
- 单一职责,每个监听器专注于特定功能
- 保持轻量化,避免耗时操作
- 确保线程安全,处理并发事件
- 正确管理资源,防止内存泄漏
- 与过滤器配合使用,构建完整处理链
实践任务
-
实现用户行为分析系统:
- 记录用户访问的页面序列和停留时间
- 分析用户最常访问的页面和功能
- 识别潜在的用户流失点
- 生成简单的统计报表
-
开发会话超时提醒功能:
- 监听会话创建事件,记录会话创建时间
- 使用 JavaScript 定时检查会话剩余时间
- 会话即将超时(如剩余 1 分钟)时提醒用户
- 提供延长会话有效期的选项
-
构建分布式在线用户统计:
- 使用 Redis 存储在线用户信息,实现跨节点共享
- 处理会话在不同节点间迁移的情况
- 实现用户在多设备登录的支持
- 提供用户强制下线的管理功能