你是否用了用 Listener 解决 “在线人数统计” 问题 —— 那时才明白,Listener 就像工厂的 “状态感应器”:当生产线(应用、Session、请求)的状态发生变化(如启动、创建、销毁)时,感应器会自动触发预设动作(如初始化资源、统计数量),无需人工干预。

在 Servlet 体系中,监听器基于事件驱动模型工作,核心是 “感知状态变化并响应”。它包含三个要素:

  • 事件源:被监控的对象(如 ServletContext、HttpSession、ServletRequest);
  • 事件对象:封装状态变化的信息(如ServletContextEventHttpSessionEvent);
  • 监听器:定义响应事件的逻辑(如contextInitialized()sessionCreated())。

这种模型就像 “智能家居系统”:门窗(事件源)被打开时,传感器(监听器)感知到状态变化,自动触发开灯、报警(响应逻辑)。早年做 OA 系统时,我曾用ServletContextListener在应用启动时加载全局配置文件,避免了在每个 Servlet 中重复读取 —— 就像 “家里总开关通电时,自动启动路由器、空调等设备”,高效且统一。

一、六大监听器的适用场景与代码示例

Servlet 规范定义了六大监听器,按监控对象可分为三类:应用级、Session 级、请求级,各自聚焦不同场景。

1. 应用级监听器(ServletContext 相关)

监控应用的创建 / 销毁、全局属性变化,适合初始化全局资源。

  • ServletContextListener:应用启动 / 关闭时触发

    j

  • // 电商应用:启动时初始化数据库连接池、加载商品分类
    public class ShopContextListener implements ServletContextListener {
        // 应用启动时执行(Tomcat启动或应用部署后)
        @Override
        public void contextInitialized(ServletContextEvent event) {
            ServletContext context = event.getServletContext();
            
            try {
                // 1. 初始化数据库连接池(全局资源)
                String dbUrl = context.getInitParameter("dbUrl");
                ConnectionPool pool = new ConnectionPool(dbUrl);
                context.setAttribute("connectionPool", pool); // 存入全局上下文
                
                // 2. 加载商品分类缓存(减少数据库查询)
                List<Category> categories = new CategoryDao(pool).queryAll();
                context.setAttribute("categoryCache", categories);
                
                System.out.println("应用初始化完成:连接池已启动,分类缓存加载完成");
            } catch (SQLException e) {
                // 初始化失败需抛出异常,阻止应用启动(避免带病运行)
                throw new RuntimeException("应用初始化失败:" + e.getMessage(), e);
            }
        }
        
        // 应用关闭时执行(Tomcat关闭或应用卸载前)
        @Override
        public void contextDestroyed(ServletContextEvent event) {
            ServletContext context = event.getServletContext();
            // 销毁连接池,释放资源
            ConnectionPool pool = (ConnectionPool) context.getAttribute("connectionPool");
            if (pool != null) {
                pool.close();
                System.out.println("应用关闭:连接池已释放");
            }
        }
    }
    
    // 配置监听器(web.xml)
    <listener>
        <listener-class>com.shop.listener.ShopContextListener</listener-class>
    </listener>
    <context-param> <!-- 全局参数,供监听器使用 -->
        <param-name>dbUrl</param-name>
        <param-value>jdbc:mysql://localhost:3306/shop</param-value>
    </context-param>

    ServletContextAttributeListener:全局属性增删改时触发适合监控全局缓存的变化(如商品分类更新时记录日志):

    public class CategoryAttributeListener implements ServletContextAttributeListener {
        @Override
        public void attributeAdded(ServletContextAttributeEvent event) {
            if ("categoryCache".equals(event.getName())) {
                System.out.println("商品分类缓存已添加,数量:" + ((List<?>) event.getValue()).size());
            }
        }
        
        @Override
        public void attributeReplaced(ServletContextAttributeEvent event) {
            if ("categoryCache".equals(event.getName())) {
                System.out.println("商品分类缓存已更新");
            }
        }
    }

2. Session 级监听器

监控 Session 的创建 / 销毁、Session 属性变化,适合用户行为分析、在线人数统计。

  • HttpSessionListener:Session 创建 / 销毁时触发

    // 电商在线人数统计
    public class OnlineUserListener implements HttpSessionListener {
        // 用原子类保证多线程安全(避免并发计数错误)
        private static final AtomicInteger onlineCount = new AtomicInteger(0);
        
        // 用户打开页面(创建Session)时在线人数+1
        @Override
        public void sessionCreated(HttpSessionEvent event) {
            int count = onlineCount.incrementAndGet();
            System.out.println("用户上线,当前在线人数:" + count);
            // 存入全局上下文,供页面展示
            event.getSession().getServletContext().setAttribute("onlineCount", count);
        }
        
        // 用户退出或Session超时(销毁)时在线人数-1
        @Override
        public void sessionDestroyed(HttpSessionEvent event) {
            int count = onlineCount.decrementAndGet();
            System.out.println("用户下线,当前在线人数:" + count);
            event.getSession().getServletContext().setAttribute("onlineCount", count);
            
            // 清理Session中的大对象,预防内存泄漏
            event.getSession().removeAttribute("largeData"); // 如临时上传的文件数据
        }
    }

  • HttpSessionAttributeListener:Session 属性增删改时触发适合监控用户行为(如记录用户加入购物车的商品):

    public class CartAttributeListener implements HttpSessionAttributeListener {
        @Override
        public void attributeAdded(HttpSessionBindingEvent event) {
            if ("cart".equals(event.getName())) {
                String userId = (String) event.getSession().getAttribute("userId");
                Map<String, Integer> cart = (Map<String, Integer>) event.getValue();
                System.out.println("用户" + userId + "添加购物车:" + cart.keySet());
            }
        }
    }

3. 请求级监听器

监控请求的创建 / 销毁、请求属性变化,适合请求日志、性能监控。

  • ServletRequestListener:请求创建 / 销毁时触发

// 记录每个请求的处理时间
public class RequestPerformanceListener implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent event) {
        // 请求开始时记录时间戳,存入请求属性
        event.getServletRequest().setAttribute("startTime", System.currentTimeMillis());
    }
    
    @Override
    public void requestDestroyed(ServletRequestEvent event) {
        ServletRequest request = event.getServletRequest();
        long startTime = (long) request.getAttribute("startTime");
        long cost = System.currentTimeMillis() - startTime;
        // 记录慢请求(耗时>500ms)
        if (cost > 500) {
            String url = ((HttpServletRequest) request).getRequestURI();
            System.out.println("慢请求警告:" + url + ",耗时:" + cost + "ms");
        }
    }
}
  • ServletRequestAttributeListener:请求属性增删改时触发适合跟踪请求参数的变化(如接口调试时记录参数修改):

    public class RequestParamListener implements ServletRequestAttributeListener {
        @Override
        public void attributeReplaced(ServletRequestAttributeEvent event) {
            String paramName = event.getName();
            Object oldValue = event.getValue();
            Object newValue = event.getServletRequest().getAttribute(paramName);
            System.out.println("请求参数" + paramName + "被修改:" + oldValue + "→" + newValue);
        }
    }

二、系统的监听器深度应用

we系统,通常监听器解决了三个核心问题,至今仍具参考价值:

应用启动初始化的 “有序加载”需求:OA 系统启动时需加载权限配置、公文模板、流程定义等多个资源,且流程定义依赖权限配置。直接在ServletContextListener中同步加载会导致启动慢(约 30 秒),且顺序易乱。

解决方案:

  • ServletContextListener配合异步线程池,将非核心资源(如公文模板)异步加载,核心资源(权限配置)同步加载并标记 “已就绪”;
  • 流程定义加载逻辑通过ServletContextAttributeListener监听 “权限配置就绪” 事件,确保依赖满足后再执行。优化后启动时间降至 10 秒,且资源加载顺序严格可控。
  1. Session 内存泄漏的 “主动预防”问题:用户上传大文件(如 100MB 的公文附件)时,临时数据存入 Session,若用户未提交直接关闭浏览器,Session 超时前大对象会占用内存,导致服务器内存暴涨。

    解决方案:

    • HttpSessionListenersessionDestroyed()中强制移除大对象(如session.removeAttribute("tempFileData"));
    • 配合HttpSessionAttributeListener,当检测到 Session 中添加超过 10MB 的对象时,立即日志报警并限制存储(如拒绝存储)。改造后,服务器内存使用率从 80% 降至 40%,再未出现 OOM(内存溢出)。
  2. 用户行为的 “全链路追踪”需求:审计用户操作(如 “张三在 10:00 查看了公文 A,10:05 下载了附件”)。

    解决方案:

    • ServletRequestListener记录请求开始 / 结束时间,关联用户 ID(从 Session 获取);
    • HttpSessionAttributeListener监控用户登录状态变化(如userId属性添加时记录登录时间);
    • 所有事件通过统一日志服务写入审计表,形成完整的用户行为链。
三、从监听器到现代框架

Servlet 监听器是事件驱动架构(EDA)的早期实践,其设计思想直接影响了后续框架(如 Spring、Vue)的事件机制。

  • 核心共性:都遵循 “事件源→事件→监听器” 的三元模型:

    • Servlet 中,HttpSession是事件源,HttpSessionEvent是事件,HttpSessionListener是监听器;
    • Spring 中,ApplicationContext是事件源,ApplicationEvent是事件,ApplicationListener是监听器;
    • Vue 中,组件是事件源,自定义事件(如click)是事件,v-on是监听器。
  • 演进升级:现代框架在 Servlet 监听器基础上扩展了 “事件总线”(如 Spring 的ApplicationEventMulticaster),支持事件异步传播、多监听器排序、事件过滤,解决了 Servlet 监听器 “同步执行、顺序难控” 的局限。但核心思想一致 ——“状态变化驱动行为,而非主动调用”

四、踩过的 “Listener 坑”
  1. 初始化阻塞应用启动某同事在ServletContextListenercontextInitialized()中同步加载 10 万条历史数据,导致应用启动超时(Tomcat 默认启动超时 60 秒)。这就像 “超市开业前,员工先把所有商品逐个擦一遍,导致顾客无法进店”。解法:核心资源同步加载(确保应用启动即可用),非核心资源异步加载(用Executors.newSingleThreadExecutor());设置合理的超时时间(如 Tomcat 的startupTimeout=120)。

  2. Session 监听器计数不准在线人数统计时,用普通int而非AtomicInteger,高并发下出现 “少计数”(如同时两个用户上线,计数只 + 1)。这就像 “两个收银员同时给同一个计数器 + 1,结果只加了 1 次”。解法:多线程环境下必须用线程安全的计数器(AtomicInteger)或加锁(synchronized)。

  3. 监听器过多导致性能损耗项目中配置了 10 个ServletRequestListener,每个请求都要触发 10 次监听,响应时间增加 50ms。这就像 “每个进入商场的顾客,要经过 10 道安检,体验极差”。解法:合并功能相似的监听器(如将 “请求日志” 和 “性能监控” 合并为一个);非必要不使用请求级监听器(高频触发,性能影响大)。

  4. 内存泄漏:监听器持有大对象ServletContextListener在初始化时将大对象(如全量用户列表)存入ServletContext,且未在contextDestroyed()中清理,导致应用卸载后类加载器无法回收,内存泄漏。这就像 “搬家时忘了清空冰箱,新住户来了冰箱还是满的”。解法contextDestroyed()中必须释放全局资源(如context.removeAttribute("largeData"));避免在全局上下文存储大对象,改用数据库或 Redis。

五、监听器的 “高效使用指南”
  1. 按场景选择监听器

    • 全局资源初始化:优先用ServletContextListener(应用启动时执行一次,资源利用率最高);
    • 用户在线统计:必用HttpSessionListener(Session 生命周期与用户会话完全匹配);
    • 请求性能监控:慎用ServletRequestListener(高频触发),可改用 Filter(逻辑更集中)。
  2. 应用初始化的 “黄金准则”

    • 顺序可控:按依赖关系加载(如先加载配置,再加载服务,最后加载缓存);
    • 异步优先:非核心资源(如静态数据缓存)用异步线程加载,避免阻塞启动;
    • 异常熔断:初始化失败时(如数据库连接不上),必须抛出异常终止启动,避免应用 “带病运行”(可用RuntimeException);
    • 资源隔离:不同初始化逻辑封装为独立组件(如ConfigLoaderCacheLoader),避免contextInitialized()代码臃肿。
  3. Session 管理与内存保护

    • 限制 Session 大小:通过HttpSessionAttributeListener监控属性大小,拒绝存储超过 1MB 的对象;
    • 主动清理资源:sessionDestroyed()中强制移除临时数据(如文件流、大 JSON),尤其是ThreadLocal变量(需调用remove());
    • 合理设置超时:通过web.xml<session-timeout>设置 Session 超时(如 30 分钟),避免长期占用内存。
  4. 性能与可维护性平衡

    • 减少监听器数量:一个监听器可处理多个相关事件(如同时实现ServletContextListenerServletContextAttributeListener);
    • 日志分级:监听器日志按重要性分级(如初始化成功用 INFO,慢请求用 WARN,异常用 ERROR),避免日志泛滥;
    • 避免业务逻辑:监听器只做 “状态响应”(如记录、初始化、清理),不处理复杂业务(如订单生成),保持单一职责。
最后小结

从 Servlet 监听器到 Spring 事件,再到微服务的事件总线,技术在迭代,但 “被动响应” 的设计思想始终未变。监听器教会我们:好的系统不是 “主动指挥每个环节”,而是 “感知变化并自动响应”—— 就像优秀的管理者,不盯着每件事,而是建立规则,让团队在变化中自主协作。早年觉得监听器 “简单无用”,后来才发现,它藏着解耦的精髓:业务代码(Servlet)无需关心 “谁在监控我”,监控逻辑(Listener)也无需侵入业务代码,两者通过 “状态变化” 间接交互,降低耦合度。这种 “少即是多” 的设计,正是软件架构从复杂到简洁的必经之路。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐