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

在 Servlet 体系中,监听器基于事件驱动模型工作,核心是 “感知状态变化并响应”。它包含三个要素:
- 事件源:被监控的对象(如 ServletContext、HttpSession、ServletRequest);
- 事件对象:封装状态变化的信息(如
ServletContextEvent、HttpSessionEvent); - 监听器:定义响应事件的逻辑(如
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 秒,且资源加载顺序严格可控。
-
Session 内存泄漏的 “主动预防”问题:用户上传大文件(如 100MB 的公文附件)时,临时数据存入 Session,若用户未提交直接关闭浏览器,Session 超时前大对象会占用内存,导致服务器内存暴涨。
解决方案:
- 用
HttpSessionListener在sessionDestroyed()中强制移除大对象(如session.removeAttribute("tempFileData")); - 配合
HttpSessionAttributeListener,当检测到 Session 中添加超过 10MB 的对象时,立即日志报警并限制存储(如拒绝存储)。改造后,服务器内存使用率从 80% 降至 40%,再未出现 OOM(内存溢出)。
- 用
-
用户行为的 “全链路追踪”需求:审计用户操作(如 “张三在 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 中,
-
演进升级:现代框架在 Servlet 监听器基础上扩展了 “事件总线”(如 Spring 的
ApplicationEventMulticaster),支持事件异步传播、多监听器排序、事件过滤,解决了 Servlet 监听器 “同步执行、顺序难控” 的局限。但核心思想一致 ——“状态变化驱动行为,而非主动调用”。
四、踩过的 “Listener 坑”
-
初始化阻塞应用启动某同事在
ServletContextListener的contextInitialized()中同步加载 10 万条历史数据,导致应用启动超时(Tomcat 默认启动超时 60 秒)。这就像 “超市开业前,员工先把所有商品逐个擦一遍,导致顾客无法进店”。解法:核心资源同步加载(确保应用启动即可用),非核心资源异步加载(用Executors.newSingleThreadExecutor());设置合理的超时时间(如 Tomcat 的startupTimeout=120)。 -
Session 监听器计数不准在线人数统计时,用普通
int而非AtomicInteger,高并发下出现 “少计数”(如同时两个用户上线,计数只 + 1)。这就像 “两个收银员同时给同一个计数器 + 1,结果只加了 1 次”。解法:多线程环境下必须用线程安全的计数器(AtomicInteger)或加锁(synchronized)。 -
监听器过多导致性能损耗项目中配置了 10 个
ServletRequestListener,每个请求都要触发 10 次监听,响应时间增加 50ms。这就像 “每个进入商场的顾客,要经过 10 道安检,体验极差”。解法:合并功能相似的监听器(如将 “请求日志” 和 “性能监控” 合并为一个);非必要不使用请求级监听器(高频触发,性能影响大)。 -
内存泄漏:监听器持有大对象
ServletContextListener在初始化时将大对象(如全量用户列表)存入ServletContext,且未在contextDestroyed()中清理,导致应用卸载后类加载器无法回收,内存泄漏。这就像 “搬家时忘了清空冰箱,新住户来了冰箱还是满的”。解法:contextDestroyed()中必须释放全局资源(如context.removeAttribute("largeData"));避免在全局上下文存储大对象,改用数据库或 Redis。
五、监听器的 “高效使用指南”
-
按场景选择监听器
- 全局资源初始化:优先用
ServletContextListener(应用启动时执行一次,资源利用率最高); - 用户在线统计:必用
HttpSessionListener(Session 生命周期与用户会话完全匹配); - 请求性能监控:慎用
ServletRequestListener(高频触发),可改用 Filter(逻辑更集中)。
- 全局资源初始化:优先用
-
应用初始化的 “黄金准则”
- 顺序可控:按依赖关系加载(如先加载配置,再加载服务,最后加载缓存);
- 异步优先:非核心资源(如静态数据缓存)用异步线程加载,避免阻塞启动;
- 异常熔断:初始化失败时(如数据库连接不上),必须抛出异常终止启动,避免应用 “带病运行”(可用
RuntimeException); - 资源隔离:不同初始化逻辑封装为独立组件(如
ConfigLoader、CacheLoader),避免contextInitialized()代码臃肿。
-
Session 管理与内存保护
- 限制 Session 大小:通过
HttpSessionAttributeListener监控属性大小,拒绝存储超过 1MB 的对象; - 主动清理资源:
sessionDestroyed()中强制移除临时数据(如文件流、大 JSON),尤其是ThreadLocal变量(需调用remove()); - 合理设置超时:通过
web.xml的<session-timeout>设置 Session 超时(如 30 分钟),避免长期占用内存。
- 限制 Session 大小:通过
-
性能与可维护性平衡
- 减少监听器数量:一个监听器可处理多个相关事件(如同时实现
ServletContextListener和ServletContextAttributeListener); - 日志分级:监听器日志按重要性分级(如初始化成功用 INFO,慢请求用 WARN,异常用 ERROR),避免日志泛滥;
- 避免业务逻辑:监听器只做 “状态响应”(如记录、初始化、清理),不处理复杂业务(如订单生成),保持单一职责。
- 减少监听器数量:一个监听器可处理多个相关事件(如同时实现
最后小结
从 Servlet 监听器到 Spring 事件,再到微服务的事件总线,技术在迭代,但 “被动响应” 的设计思想始终未变。监听器教会我们:好的系统不是 “主动指挥每个环节”,而是 “感知变化并自动响应”—— 就像优秀的管理者,不盯着每件事,而是建立规则,让团队在变化中自主协作。早年觉得监听器 “简单无用”,后来才发现,它藏着解耦的精髓:业务代码(Servlet)无需关心 “谁在监控我”,监控逻辑(Listener)也无需侵入业务代码,两者通过 “状态变化” 间接交互,降低耦合度。这种 “少即是多” 的设计,正是软件架构从复杂到简洁的必经之路。
更多推荐
所有评论(0)