JavaWeb 30 天入门:第二十三天 —— 监听器(Listener)

在前二十二天的学习中,我们已经掌握了 JavaWeb 开发的核心技术栈,包括 Servlet、JSP、会话管理、过滤器、AJAX、分页排序等。今天我们将深入学习 JavaWeb 三大组件的最后一个 ------监听器(Listener)

监听器是一种特殊的组件,它能够监听 Web 应用中特定事件的发生(如应用启动、会话创建、属性变化等),并在事件发生时执行相应的处理逻辑。监听器在系统初始化、资源管理、状态监控等方面有着不可替代的作用,是构建响应式 Web 应用的重要工具。

监听器概述

什么是监听器

监听器(Listener) 是 JavaWeb 中用于监听 Web 应用内部事件并作出响应的组件。它基于观察者模式设计,包含两个核心角色:

  • 事件源:产生事件的对象(如 ServletContext、HttpSession、ServletRequest)
  • 监听器:监听事件源产生的事件,并在事件发生时执行特定操作

监听器的工作流程:

  1. 监听器注册到事件源,声明要监听的事件
  2. 当事件源发生特定事件时,自动通知所有注册的监听器
  3. 监听器接收到事件通知后,执行预设的处理逻辑

监听器的作用

监听器主要用于解决以下问题:

  • 监控 Web 应用的生命周期(启动、关闭)
  • 跟踪用户会话的创建与销毁
  • 监听请求的产生与结束
  • 监测域对象(ServletContext、HttpSession、ServletRequest)中属性的变化
  • 实现组件间的解耦通信(基于事件机制)
  • 资源的初始化与释放(如数据库连接池、缓存)

监听器的优势

  1. 事件驱动:基于事件响应模式,无需主动调用,自动触发
  2. 解耦设计:监听器与事件源分离,降低组件间耦合度
  3. 全局监控:可对整个应用、所有会话或请求进行统一监控
  4. 生命周期管理:精确控制资源在合适的时机初始化和释放
  5. 灵活性:可根据需要注册或移除监听器,动态调整系统行为

监听器核心接口

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. 实现监听器的步骤

实现一个监听器通常需要以下步骤:

  1. 编写类实现相应的监听器接口
  2. 重写接口中的事件处理方法
  3. 注册监听器(通过注解或 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. 监听器与过滤器的配合使用

监听器和过滤器经常配合使用,形成完整的处理链:

  1. 初始化阶段ServletContextListener初始化全局资源
  2. 请求阶段
    • ServletRequestListener记录请求开始时间
    • 过滤器链处理请求(编码、权限、安全等)
    • 目标资源处理请求
  3. 响应阶段
    • 过滤器链处理响应
    • ServletRequestListener记录请求结束时间,计算处理耗时
  4. 会话管理
    • HttpSessionListener跟踪用户会话
    • 属性监听器监控会话数据变化
  5. 销毁阶段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>

总结与实践

知识点回顾

  1. 监听器基础

    • 监听器用于监听 Web 应用中的事件,基于观察者模式
    • 三大类监听器:生命周期监听器、属性监听器、对象感知监听器
    • 核心接口:ServletContextListenerHttpSessionListenerServletRequestListener
  2. 监听器应用

    • ServletContextListener:应用初始化、资源管理
    • HttpSessionListener:在线用户统计、会话管理
    • ServletRequestListener:请求监控、性能分析
    • 属性监听器:跟踪域对象属性变化
    • 会话对象感知监听器:对象与会话的绑定 / 解绑、钝化 / 活化
  3. 最佳实践

    • 单一职责,每个监听器专注于特定功能
    • 保持轻量化,避免耗时操作
    • 确保线程安全,处理并发事件
    • 正确管理资源,防止内存泄漏
    • 与过滤器配合使用,构建完整处理链

实践任务

  1. 实现用户行为分析系统

    • 记录用户访问的页面序列和停留时间
    • 分析用户最常访问的页面和功能
    • 识别潜在的用户流失点
    • 生成简单的统计报表
  2. 开发会话超时提醒功能

    • 监听会话创建事件,记录会话创建时间
    • 使用 JavaScript 定时检查会话剩余时间
    • 会话即将超时(如剩余 1 分钟)时提醒用户
    • 提供延长会话有效期的选项
  3. 构建分布式在线用户统计

    • 使用 Redis 存储在线用户信息,实现跨节点共享
    • 处理会话在不同节点间迁移的情况
    • 实现用户在多设备登录的支持
    • 提供用户强制下线的管理功能
相关推荐
天高云淡ylz2 小时前
子网掩码的隐形陷阱:为何能ping通却无法HTTPS访问
开发语言·php
@小匠2 小时前
Spring Cache 多租户缓存隔离解决方案实践
java·spring·缓存
DisonTangor2 小时前
字节开源 OneReward: 通过多任务人类偏好学习实现统一掩模引导的图像生成
学习·ai作画·开源·aigc
智码看视界3 小时前
老梁聊全栈系列:(阶段一)架构思维与全局观
java·javascript·架构
黎宇幻生3 小时前
Java全栈学习笔记33
java·笔记·学习
2501_926227944 小时前
.Net程序员就业现状以及学习路线图(五)
学习·.net
希望20174 小时前
Golang Panic & Throw & Map/Channel 并发笔记
开发语言·golang
朗迹 - 张伟4 小时前
Golang安装笔记
开发语言·笔记·golang
yzx9910134 小时前
生活在数字世界:一份人人都能看懂的网络安全生存指南
运维·开发语言·网络·人工智能·自动化