WAR 能启动,不等于资源能用

采购用例通过存储端口访问数据库。开发机上能用 DriverManager 建立连接,不表示部署后的 WAR 已经使用服务器管理的数据源;健康端点能返回 ready,也不表示每个采购调用都拿到了连接。如果数据库口令、驱动 JAR 和连接池配置各自散落在业务类里,重启、更换配置以及追踪失败会变得困难。应用服务器提供组件环境、资源引用和受管调用路径,代价是必须区分:声明了什么、服务器映射了什么、实际调用经过了什么。

本篇源码、迁移、脚本与测试可从固定版本完整归档取得;版本 1b08ada,SHA-256 见源码清单。

累计工程中,examples/javaee-enterprise/deploy/server.xml 是 Open Liberty 26.0.0.5 的实验配置,开启 jakartaee-11.0 和 jdbc-4.3,注册 pgJDBC 42.7.7 驱动库,并把名为 jdbc/Procurement 的服务器数据源指向独立的 javaee_lab 库。examples/javaee-enterprise/adapters-jdbc/src/main/java/blog/javaee/jdbc/JdbcRequestStore.java 的 @Resource(lookup = "jdbc/Procurement") 指向该名字;它使用 javax.sql.DataSource 获取 Connection,以 try-with-resources 释放借用的 JDBC 资源。这里 javax.sql 来自 Java SE 21,不应为迎合 Jakarta 命名空间改成不存在的 jakarta.sql。配置本身不是运行证据,但另有一个 Web 探针确实取得过数据源连接;这不证明 JdbcRequestStore 的写入或事务运行成功。

教学基线是 Jakarta EE 11 Platform / Java SE 21 / Open Liberty 26.0.0.5 / PostgreSQL 16.15 / pgJDBC 42.7.7。Jakarta EE 11 的平台最低要求是 Java SE 17;选择 Java 21 是本工程的版本冻结,不是规范下限。特定 Open Liberty 构建的兼容声明与本 WAR 的实际部署须分别核验。examples/javaee-enterprise/scenarios/00-health.sh 只请求静态健康入口;examples/javaee-enterprise/scenarios/05-datasource.sh 请求真实 /procurement/api/db-check。服务器日志记录应用启动及 jakartaee-11.0、jdbc-4.3 等特性安装,HTTP 响应记录成功连接。当前工程没有本章拟议的最小无状态会话 bean、拦截器和故障注入脚本。健康检查不证明数据库连接;数据库检查不证明无状态 bean 的调用。

名字是部署契约,连接是运行时资源

JNDI(Java Naming and Directory Interface)提供查询名字的 API;在 Jakarta EE 应用中,组件环境把声明的引用与部署时的实际资源关联。全局服务端资源名、组件私有的 java:comp/env 名与应用代码中的 lookup 字符串不能随意互换。这里真实代码使用 @Resource(lookup = "jdbc/Procurement"),真实 Open Liberty 配置写 jndiName="jdbc/Procurement",是针对这一部署配置配对的名字,不应泛化成每一台 EE 11 服务器都预置该资源。若将来改成可移植的逻辑引用,例如 java:comp/env/jdbc/Procurement,还要声明并部署映射;本仓库尚未采用那套名字。不要把示意名字直接复制进现有注入点后宣称它已配置。

注入的是 DataSource 访问入口,不是为整个应用永久占住的一条 Connection。调用 getConnection() 才可能触发资源分配、池借用与数据库建连;归还逻辑连接也不等于物理连接必然断开。资源名不存在时,容器可能在部署校验时报错,也可能延迟到首次访问资源时暴露故障,具体时间与异常包装以冻结实现的真实日志为准。数据库地址可解析但账号错误,则更可能在获取连接的路径失败;把所有错误一概称作“JNDI 注入失败”会失去定位信息。@Resource 与 CDI 的 @Inject 也不能互换概念:前者声明服务器资源引用,后者在类型及限定符集合上解析受管 bean。DataSource 和 RequestStore 即使沿同一调用链出现,也承担不同契约。

实际排错可按部署件、资源名、驱动、连接、SQL 的顺序缩小范围。先检查 WAR 是否是本次构建的产物及注入点确实进入部署包;再比对服务端实例读取的配置和 lookup 名称;随后确认驱动库在该实例中可见,最后才尝试数据库账号及最小查询。驱动 JAR 存在于机器的 Maven 缓存,不表示服务器已经按 libraryRef 装载它。反过来,一个成功的数据库 TCP 连接,也不表示驱动加载、用户身份、schema 和查询都已正确。把每一步的退出码和原始日志留在同一个证据时间窗,能避免拿前一轮部署的成功记录为本轮错误配置背书。

运行前要给驱动库路径、WAR 路径及数据库凭证提供本地环境变量;deploy/server.xml 中的 ${env.JAVAEE_DRIVER_DIR}、${env.JAVAEE_WAR}、${env.JAVAEE_LAB_USER} 和 ${env.JAVAEE_LAB_PASSWORD} 必须指向隔离环境中的真实值。真实代码 examples/javaee-enterprise/webapp/src/main/java/blog/javaee/web/DatabaseCheckResource.java 使用 @Resource(lookup = "jdbc/Procurement") 注入 DataSource,以 try-with-resources 借用连接并执行 SELECT current_database();只有查询结果为 javaee_lab 时才返回 connected。它不向 HTTP 返回账号、密码或数据库标识。源码上能够看见关闭路径,已有证据表明这一次只读查询成功,但没有池借还指标、写入提交或故障后恢复记录;这些仍须另测。正式认证和权限尚未实现,诊断端点只适合绑定回环地址的隔离教学环境。

原始记录位于 writing-plans/javaee-enterprise/verification/20261004T061900Z-pg16-foundation/:server-final.stdout.txt 载有应用启动和已安装功能清单,build-3.stdout.txt 载有 Maven reactor BUILD SUCCESS;datasource.headers.txt 记有 HTTP 200,datasource.body.txt 为 connected,datasource.stdout.txt 保存场景脚本的请求路径与输出,datasource-exit-code.txt 为 0。同目录的 db-version.txt 记有专用库和 PostgreSQL 16.15。这些文件支撑当时部署物返回一次只读探针的响应;该目录未保存构建与实际部署 WAR 的字节摘要,不能单凭当前探针源码反推当时的部署代码路径。HTTP 正文没有显式打印库名,库名检查依赖当前探针源码对 current_database() 的比较;必须先完成源码、构建包与部署包的哈希绑定才能将两种证据关联,更不能由此证明池借还、业务事务或所有数据库操作。

租户是业务权限问题,不是 JNDI 名称本身能解决的问题。JdbcRequestStore.find 的 SQL 带 tenant_id 条件;这不能证明传入的租户身份可信,也不能保证其它路径都做了对象权限校验。缺失正式认证和授权时,数据库探针只准在回环地址、隔离环境使用。不要为了让本章“可访问”就把采购用例暴露成接受任意 tenant ID 的 HTTP 接口;第 24–26 篇才负责可信身份和授权矩阵。

同一方法,受管调用和直接构造不同

Jakarta Enterprise Beans 的无状态会话 bean(@Stateless)是容器管理的业务组件。它可以作为一层很薄的操作入口,注入 DataSource,在业务方法中取得连接并执行只读探测,再由 try-with-resources 释放连接。无状态指的是不把某个客户端的会话状态固定保存在 bean 实例中,不表示 bean 方法无法使用局部状态,也不表示可在字段上安全保存本次采购的租户信息。实例池、调用并发、事务属性和拦截配置的具体效果必须在受管调用上观察;不要把容器内部池大小推测成规范承诺。

当前工程的 examples/javaee-enterprise/application/src/main/java/blog/javaee/application/ProcurementUseCases.java 是 @ApplicationScoped CDI bean,而不是 @Stateless。它以 @Transactional 标记若干采购操作,RequestStore 由实现 JdbcRequestStore 提供。数据库探针只调用自己的 JAX-RS @GET 方法,不经过该采购用例或会话 bean;因此不能推出采购调用已经经过事务拦截,更不能推出连接参与了容器事务。拟议的另一条实验路径是新增最小 @Stateless 只读探测 bean,并由一个仅限隔离环境的诊断入口经容器引用调用。若改为 new ResourceProbeBean(),@Resource 或 @EJB 字段通常不会由容器注入,也没有会话 bean 的受管调用语义。即使手动传入 DataSource 让普通对象执行了 SQL,它仍不是经过会话 bean 的调用。

拦截器为受管调用提供可复用的横切行为。EE 11 平台包含 Jakarta Interceptors 2.2:可在适当的受管组件上通过拦截声明把方法调用送进 @AroundInvoke,调用链通过 InvocationContext.proceed() 继续。示例可用拦截器记录不含凭证的调用 ID、被调用的方法和异常结果,在正常和故障请求各记录一次;不能让诊断拦截器吞掉 SQL 异常后返回成功。@PostConstruct 和 @PreDestroy 用来观察组件生命周期,不意味着每个 HTTP 请求都会触发一次无状态 bean 的创建/销毁。@Transactional 是事务拦截语义,不是给任意 new 出来的对象加一个可调用的魔法方法;会话 bean 也有独立的事务属性规则。二者具体传播、回滚和异常矩阵留到第 18–19 篇,以最终数据库行集合核验,不能套用 Spring 代理的结论。

拦截路径还必须明确入口。若外部以容器管理的会话 bean 引用发起业务调用,可以观察选定拦截器及受管资源;若新建普通对象直接调用同名方法,则不应期望相同容器拦截行为。把“类上有 @Stateless/拦截注解”“容器已创建目标”“本次方法确实通过代理或容器引用”三个断言拆开。至于 bean 内部调用自己的其它方法,能否经过相同的边界不可由外部调用成功类推;实验应限定在外部进入受管引用的路径,内部调用再单独验证。尤其不能通过“拦截日志没打印”直接断定数据库没有执行,仍须核对执行路径与查询结果。

还应避免把一次拦截当作全部资源管理的替代物。拦截器适合记录请求关联和失败阶段,但不应该替 DAO 关闭 ResultSet、PreparedStatement、Connection,也不该吞掉原始数据库异常来伪造业务成功。JdbcRequestStore 当前用 try-with-resources 关闭语句、结果集及连接;这说明代码写了释放路径,尚不是在服务器池里观察到借还平衡。无状态 bean 的创建/回收也不是“一次调用对应一个实例”,所以日志里看见两个调用 ID 相同,不能推出所有调用都串行或有客户端粘性。需要对照资源持有时间、并发请求和最终数据库状态;本章只核对可观测的最小只读路径。

实验合同:从名字到受管入口

真实可执行的构建和正常路径脚本在 examples/javaee-enterprise/README.md、examples/javaee-enterprise/scenarios/00-health.sh 与 examples/javaee-enterprise/scenarios/05-datasource.sh。在已配置的专用 javaee_lab 库与冻结服务器上,可用现有探针复查只读 SQL;拟议的 /_diag/resources 不存在,是新增无状态 bean 及拦截器后才可调用的另一诊断入口,不在以下命令中。用全新本地专用库和角色执行已有 db/migrations/001-initial.sql,保存服务器配置、驱动版本和迁移退出码;绝不对共用的 JPA 库或生产库动手。实际通过的路径、尚未运行的路径及各自证据见 服务器资源与拦截实验合同。

1
2
3
4
5
cd examples/javaee-enterprise
../hibernate-lab/mvnw -B -ntp -f "$PWD/pom.xml" clean verify
bash scenarios/00-health.sh
bash scenarios/05-datasource.sh
curl -sS -i http://127.0.0.1:9085/procurement/api/db-check

上述四条命令中的资源脚本与 curl 都对应已实现的真实端点;有一次带原始记录的成功观测。用 JAVAEE_PORT=9085 运行脚本,其他服务器变量按 examples/javaee-enterprise/README.md 配置;本轮文章编辑没有重跑构建或部署。curl 独立调用的预期是 HTTP 200 与 connected,而脚本还核对正文。**不要把上面的 HTTP 200 扩写成受管 EJB 已验证。**后续新增 @Stateless 探针和拦截器,才要求该路径得到只读 SQL 确定值、匹配调用 ID 的拦截事件和可观察的连接借还;保存原始响应和日志。若想证明连接可归还,可在有界小池中连续借用并释放,再观察后续借用;不能以单次成功断言池永不耗尽。

失败实验只在隔离配置副本中把 jndiName="jdbc/Procurement" 改为不存在的名字,保留现有资源探针的 @Resource(lookup = "jdbc/Procurement")。重新部署后先记录是否部署失败;若部署成功,则请求真实的 /procurement/api/db-check,记录首次资源获取失败的调用位置。两种失败时机均可接受,断言是该调用不得静默连接到旧资源并返回 connected。再恢复正确名字并复测,核对实际加载的服务器配置,不能把恢复当作既成事实。另一组尚未实现的对照是直接构造普通对象与受管 @Stateless bean 比较拦截事件;直接构造对象未注入字段的异常只证明对象没配置完整。若要对比相同查询,应显式传入测试用 DataSource 并仅比较拦截事件,避免把 SQL 差异归咎于容器调用。改错资源名、凭证错误、会话 bean 及拦截器的对照均 NOT_RUN;本次仅有正确资源名的正常路径证据。

诊断失败还有必要留一个对照组:名字正确但数据库凭证故意无效时,应用也可能启动,失败却应指向连接获取或认证,而不是资源引用解析。两种注入错误仅改变一个条件,并保存脱敏异常链;HTTP 对外可统一返回受限的服务错误,内部证据仍需保留根因。恢复凭证时先确认数据库专用角色权限,再重新测只读 SQL;若数据库会话残留,不能仅凭健康端点成功宣布恢复。让错误配置只作用于隔离实例,避免误停其他人共用的服务器或破坏真实业务库。

两道有解的练习

练习一: 健康检查返回 200,资源探针部署却报找不到 jdbc/Procurement。有人建议把 @Resource 改成 @Inject DataSource,因为“同是注入”。这能作为修复吗?给出最小定位顺序。

解: 不能直接替换,两种注入有不同解析规则。先核对当前部署使用的 server.xml 和 WAR,再核对 @Resource(lookup = "jdbc/Procurement") 与服务端 jndiName、驱动路径和环境变量;记录是部署期解析失败还是首次 getConnection() 失败。名字一致而数据库拒绝连接,转查地址、角色和口令,不要继续更改 CDI 限定符。修复后用现有 /api/db-check 调用执行只读 SQL;/api/health 的 200 只作为 Web 入口正常的附属证据。正确配置下现有探针有一次成功记录,题设的错误配置与恢复实验仍为 NOT_RUN。

练习二: 给计划中的 @Stateless 探针加方法拦截器。通过容器引用调用打印一条入口日志,手工 new 一个同类型对象并给它传入测试连接后调用同名方法,SQL 都得到同样的值。可否说两次调用拥有相同事务、安全和资源管理契约?

解: 不可。查询值只能说明两条路径在该输入下读到了相同数据;直接构造的实例没有经过会话 bean 的受管入口,也不能因手动塞入依赖而获得容器拦截、身份或事务属性。期望受管路径有匹配调用 ID 的拦截事件、直构路径没有;在触发异常时要分别记录异常和调用事件。若要证明事务效果,必须增设受控写入并在第 18–19 篇检查提交后的行集合,不能由只读探针或一条入口日志外推。本练习是推导,不是已运行的结果。

适用范围与版本资料

本章可把服务器能力拆成可核验的三个层次:名字与部署配置相配,资源确实能借用,方法确实经受管入口。一次 db-check 支持前两层在当前配置下的只读路径,不能充当第三层“经过 @Stateless 拦截器”的证明。这里没有验证会话 bean 的事务提交或回滚、完整企业安全权限、连接池极限和多服务器可移植性;以后变更运行时,要重新核验资源映射和部署行为,而非复用一次健康报告。

官方版本入口:Jakarta EE 11 Platform:组件环境及资源引用、Jakarta Enterprise Beans 4.0 核心规范、Jakarta Interceptors 2.2、Jakarta Annotations 3.0、Java SE 21 javax.sql 与 Open Liberty 26.0.0.5 Jakarta EE 11 功能文档。规范、厂商文档和本应用原始运行记录各回答不同的问题:只读数据源正常路径有记录,其它路径保留 NOT_RUN。