5.3 Listener与JNDI:事件与资源


文档摘要

5.3 Listener 与 JNDI:事件与资源 本节摘要:管道上还有两位不拦请求的后勤官。Listener 是事件广播员,容器与应用里发生的大事——启动、停止、会话建立与销毁、请求到来——都会向登记过的监听器广播;JNDI 是资源调度台,把数据源、环境变量登记在容器里,应用按名字查找使用。本节讲清监听器的三类事件与书写方式、JNDI 的全局与应用级两种定义、查找代码与容错要点。 前两节的组件都在拦请求,本节两位站在路边服务。别小看后勤:缓存预热、连接池借还、配置读取,这些"启动时干一次、运行时反复用"的事全靠它们——第 7 章讲内存泄漏排障时,监听器没清理资源是头号惯犯。

5.3 Listener 与 JNDI:事件与资源

本节摘要:管道上还有两位不拦请求的后勤官。Listener 是事件广播员,容器与应用里发生的大事——启动、停止、会话建立与销毁、请求到来——都会向登记过的监听器广播;JNDI 是资源调度台,把数据源、环境变量登记在容器里,应用按名字查找使用。本节讲清监听器的三类事件与书写方式、JNDI 的全局与应用级两种定义、查找代码与容错要点。

前两节的组件都在拦请求,本节两位站在路边服务。别小看后勤:缓存预热、连接池借还、配置读取,这些"启动时干一次、运行时反复用"的事全靠它们——第 7 章讲内存泄漏排障时,监听器没清理资源是头号惯犯。

事件广播员:三类事件一张表

监听器的世界按事件源分三类:容器生命周期事件(应用启动停止)、会话事件(会话建立销毁属性变化)、请求事件(请求开始销毁)。接口全是规范预定义的,实现哪个就收哪类广播。

接口 广播时机 典型用途
ServletContextListener 应用初始化与销毁 缓存预热、连接池自建、清理现场
HttpSessionListener 会话建立与销毁 在线人数统计、会话配额
ServletRequestListener 请求开始与结束 全链路追踪埋点、慢请求计数

写一个监听器把三类事件都接住,日志里就能看到应用的呼吸节律。

// 应用内的监听器:三类事件全登记 import jakarta.servlet.*; import jakarta.servlet.http.*; import jakarta.servlet.annotation.WebListener; @WebListener public class AppListener implements ServletContextListener, HttpSessionListener, ServletRequestListener { @Override public void contextInitialized(ServletContextEvent sce) { // 应用启动时执行一次:预热缓存 建线程池都在这里 System.out.println("应用启动完成 名字是 " + sce.getServletContext().getContextPath()); } @Override public void contextDestroyed(ServletContextEvent sce) { // 应用销毁时执行:关线程池 清缓存 补上这句能少很多内存泄漏 System.out.println("应用即将销毁 清理现场"); } @Override public void sessionCreated(HttpSessionEvent se) { System.out.println("新会话建立 编号 " + se.getSession().getId()); } @Override public void sessionDestroyed(HttpSessionEvent se) { System.out.println("会话销毁 " + se.getSession().getId()); } @Override public void requestInitialized(ServletRequestEvent sre) { System.out.println("请求开始 " + ((HttpServletRequest) sre.getServletRequest()).getRequestURI()); } @Override public void requestDestroyed(ServletRequestEvent sre) { System.out.println("请求结束"); } }

一个工程细节值得强调:销毁回调不是摆设。contextDestroyed 里关闭自建线程池、释放外部连接,是防类加载器泄漏的第一道防线——热部署几次后内存涨、线程数不减,八成是这里没清理。另外 ServletContextEvent 之外还有容器层的 ServerListener 一族(在 server.xml 配置),监听整个 Server 的启停,适合做实例级初始化,与应用级监听器分属两个世界,别混用。

资源调度台:JNDI 的两级登记

JNDI 的思想一句话:资源归容器管,应用按名取。数据库连接池为什么不该让应用自建?因为三个应用各自建池,数据库连接总数失去控制;由容器统一登记一个池,总量可控、配置集中、更换数据库只动容器一处。

两级登记先看全局级:资源定义在 server.xml 的 GlobalNamingResources 里,所有应用可见,再经 ResourceLink 放行给具体应用。

<!-- server.xml:全局资源登记 --> <GlobalNamingResources> <Resource name="jdbc/sharedb" auth="Container" type="javax.sql.DataSource" driverClassName="org.postgresql.Driver" url="jdbc:postgresql://db-internal:5432/shared" username="shared_app" password="从密钥管理读取" maxTotal="30" maxIdle="8" maxWaitMillis="6000"/> </GlobalNamingResources> <!-- 应用对应的 Context 片段里放行 --> <Context docBase="order" path="/order"> <ResourceLink name="jdbc/sharedb" global="jdbc/sharedb" type="javax.sql.DataSource"/> </Context>

应用级更简单:4.3 已在应用的 context.xml 里写过 Resource 定义,作用域仅限本应用。选型看共享需求——一个库多个应用用就走全局加放行,独享就写在应用侧。

应用侧的查找代码是标准三步:初始化命名上下文、按名查找、强转使用。

// 在 Servlet 或监听器里按名取数据源 import jakarta.annotation.Resource; import javax.sql.DataSource; import java.sql.Connection; public class OrderDao { // 方式一:注解注入 容器代劳查找(推荐) @Resource(name = "jdbc/orderdb") private DataSource dataSource; public Connection borrow() throws Exception { return dataSource.getConnection(); } }
// 方式二:手动 JNDI 查找 适合非托管环境 import jakarta.servlet.ServletContext; import javax.naming.InitialContext; import javax.sql.DataSource; public DataSource lookup() throws Exception { InitialContext ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/orderdb"); }

lookup 的名字有前缀讲究:应用私有的命名空间固定以 java:comp/env 开头,后面接资源名。查不到时的报错分两种:名字不对报 NameNotFoundException,定义了对不上类型报转换异常——前者查拼写与 ResourceLink,后者查 type 属性与实际实现类是否一致。

⚠️ 常见坑:驱动包只放进应用的 WEB-INF 却用了全局 Resource——全局资源的类加载器看不见应用的库,启动时报驱动类找不到。全局资源的依赖要放容器的 lib,应用级资源才可用应用的 lib。又是"谁的类放谁家"。

后勤视角的收束

监听器与 JNDI 都不碰请求的主流程,却决定应用能不能"干净地活、体面地死"。三个带走要点:启动做加法(预热、建池),销毁做减法(关池、清缓存),资源交容器(连接池全局统管,应用按名取)。这三条做全,第 7 章讲到的热部署泄漏、连接耗尽两类故障基本与你绝缘。

  • 监听器三类事件:容器、会话、请求,接口规范预定义,实现即登记。
  • 销毁回调必须清理:contextDestroyed 是防泄漏第一防线。
  • JNDI 两级登记:全局资源加放行管共享,应用级定义管独享。
  • 查找名字带前缀:java:comp/env 之后接资源名,报错先查拼写再查类型。
  • 类归属依旧:全局资源的驱动进容器 lib,应用级的才进应用。

后勤齐备,请求终于要进店了。第 6 章是终点站:过滤器链怎么一环环传递、JSP 如何被编译成 Servlet、WebSocket 如何升级连接、Spring Boot 为什么把 Tomcat 装进了应用里。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U