遇到javax.naming.NameNotFoundException: Name "com" not found的时候,我正接手一个用 Jersey 1.x 写的老接口服务。服务在本地开发环境跑得很正常,一部署到测试环境的应用服务器上,启动日志就炸了,异常明明白白写着“com”找不到。可项目里包名明明就是com.example.rest,类也都存在。后来我把这条异常和 Jersey 1.x 的资源发现机制放在一起研究,才搞明白真正的矛盾点是包扫描器和服务器 JNDI 命名服务之间的碰撞。这篇文章就从报错现场讲起,把根因、定位过程和几种落地修复方案完整写出来,给还在维护 JAX-RS 老项目的同行做个参考。
1. 报错现场与“com”这个名称的真实含义
1.1 典型堆栈长什么样
先还原一下报错现场。项目没有改任何业务代码,只是在应用服务器上重新发布 WAR 包,启动时控制台直接出现这样的堆栈:
javax.naming.NameNotFoundException: Name "com" not found at [应用服务器容器的命名服务实现] at javax.naming.InitialContext.lookup(InitialContext.java:由环境决定) at com.sun.jersey.core.spi.scanning.PackageNamesScanner.scan(PackageNamesScanner.java:环节之一) at com.sun.jersey.api.core.PackagesResourceConfig.<init>(PackagesResourceConfig.java:环节之一) at com.sun.jersey.server.impl.application.WebApplicationImpl._initiate(WebApplicationImpl.java:环节之一) at com.sun.jersey.spi.container.servlet.ServletContainer.init(ServletContainer.java:环节之一)第一次看到这个异常时,我第一反应是“项目里是不是少了什么com命名的资源”。于是翻遍了web.xml、pom.xml、资源目录,都没发现问题。后来才意识到,这个com不是某个类名,也不是某个配置项,而是一个 JNDI 名称里的第一级路径。
这里的NameNotFoundException是 JNDI 的经典异常。消息里说“Name 'com' not found”,意思是 JNDI 命名系统在解析某个名称时,第一步要定位com这个子上下文,但当前的命名环境里压根没有这个节点。换句话说,不是你的代码里缺了什么东西,而是服务器在某个环节拿“com”这样一个字符串去做了 JNDI 查找。
1.2 JNDI 在找的“com”到底是什么
JNDI 是 Java 平台里的命名和目录服务,最常见的使用场景是在 Java EE 环境里查找数据源、消息队列或自定义资源,比如java:comp/env/jdbc/primaryDataSource。当调用InitialContext.lookup("java:comp/env/jdbc/primaryDataSource")时,命名服务会按分隔符逐级解析:先找java,进入comp,再进入env,以此类推。如果某一级不存在,就会抛出NameNotFoundException,并且消息只提示缺失的那一级名字。
所以在我们的场景里,异常消息是“Name 'com' not found”,可以推断出当时 JNDI 被调用的名称至少是以com开头的,很可能就是com.example.rest这样一段和 Java 包名对应的字符串。JNDI 的常见实现里,既能接受/作为分隔符,也能接受.作为分隔符,因此com.example.rest会被拆成com、example、rest三个层级去解析。服务器的根命名上下文里没有com,于是第一步就失败了。
这里有个容易误判的点:很多人以为是代码里某个叫com的包没有被打进 WAR。但 Java 包名只是代码组织方式,和 JNDI 命名空间是两套东西。一个类可以存在于com.example.rest包下,同时 JNDI 环境里完全可以不存在com这个节点。两者唯一的关系是字符串长得像,也正是这个“长得像”,让 Jersey 1.x 的包扫描过程在特定服务器上出了问题。
1.3 是包扫描的锅,还是容器环境的锅
把这个问题归类后,我倾向于这样描述:业务代码本身是对的,Jersey 1.x 的默认扫描机制也是按设计在运行,问题出在“Jersey 1.x 的扫描器 + 特定应用服务器的类加载器/命名服务实现”这个组合上。
在普通 Servlet 容器里,ClassLoader.getResources("com/example/rest")会去文件系统或 JAR 包里寻找资源,返回一个 URL 枚举。这类操作和 JNDI 没有关系。但在一些老牌 Java EE 应用服务器里,web 应用的类加载器并不是简单的文件/JAR 聚合器,它可能被设计成与服务器的命名系统联动。类加载器在处理资源路径时,会先让命名服务尝试解析这个路径;如果命名服务里没有对应节点,就会直接把异常抛出来,而不是默默返回空枚举。
打个比方:你让同事去档案柜拿com/example/rest的文件夹,正常流程是直接走到柜子前翻。但这位同事的流程是先查一本通讯录,通讯录里没有com。于是他连档案柜都不去开,回头告诉你“找不到 com”。放在这个问题里,JNDI 就是那本通讯录,类加载器就是那位严格遵守流程的同事。
2. 根因拆解:Jersey 1.x 包扫描怎么把 JNDI 给卷了进来
2.1 包扫描的入口:com.sun.jersey.config.property.packages
绝大多数 Jersey 1.x 应用会通过web.xml里的ServletContainer初始化,并配置包扫描参数。最常见的写法是这样的:
<servlet> <servlet-name>jersey</servlet-name> <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class> <init-param> <param-name>com.sun.jersey.config.property.packages</param-name> <param-value>com.example.rest</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>jersey</servlet-name> <url-pattern>/resources/*</url-pattern> </servlet-mapping>这个com.sun.jersey.config.property.packages参数会让 Jersey 在启动时把指定包下的所有类都扫描一遍,凡是标注了@Path或@Provider的类都会被纳入 REST 应用。对老项目来说,这种写法非常省事,新增接口类后不需要改配置,发布后自动被发现。
但省事的代价就在扫描那一刻。Jersey 1.x 的PackageNamesScanner拿到com.example.rest后,把点号转成斜杠,得到资源路径com/example/rest,然后交给类加载器枚举该路径下的.class文件。在标准实现里,这一步只是资源遍历;在带 JNDI 联动的类加载器里,com/example/rest会被当成一个命名路径去解析,于是异常就在 Jersey 的初始化阶段冒了出来。
2.2 类加载器枚举资源时为什么会被命名服务接管
要理解这一点,得先看 Java EE 应用服务器为什么要把类加载器做得这么复杂。生产环境通常需要支持多应用隔离、共享库、热部署、模块化部署,所以服务器会自定义 web 应用的类加载器,有的还会加上一层“资源提供者”来做权限控制或模块路由。某些实现里,这层资源提供者与 JNDI 共用同一套路径解析逻辑。
当 Jersey 调用Thread.currentThread().getContextClassLoader().getResources("com/example/rest")时,这层逻辑接收到的参数就是一个字符串路径。如果实现方把这字符串交给命名服务去“解析”,且命名服务又很严格,那结果就是NameNotFoundException。尤其是老版本的商用 Java EE 服务器,为了保持自己的模块命名体系兼容,类加载器里的 JNDI 联动往往非常激进,任何一个开头的路径名都会被送到命名空间里验证一遍。
这种问题有一个很典型的特征:同样的 WAR 包扔到简单的嵌入式 Servlet 容器里,一切正常;扔到复杂的商用应用服务器里,启动就报错。原因是后者的类加载器里多了命名服务联动,而前者的类加载器就是老老实实查文件、查 JAR。
2.3 最容易中招的部署形态
根据我查过的资料和实际踩坑经验,下面几种情况最容易触发这个异常:
- 项目使用
com.sun.jersey.config.property.packages做自动扫描,且扫描的顶级包名是com、org这类在 JNDI 根上下文里几乎不可能存在的命名。 - 应用部署在老版本 Java EE 商用服务器上,这类服务器的类加载器和 JNDI 命名服务集成程度较高。
- WAR 包采用
WEB-INF/classes目录形态,而不是把所有类打进一个或多个 JAR,扫描器需要真实遍历目录资源,更容易触发容器资源层的处理逻辑。 - 同一个应用在本地开发环境使用内置容器运行,和生产服务器的类加载器实现完全不同,导致本地永远复现不了。
如果你正在维护的项目同时命中上面两三条,那这个异常基本可以确认是环境耦合问题,不是代码逻辑问题。
3. 从复现到锁定:一个完整的定位过程
3.1 第一步:把完整堆栈和 web.xml 拍在桌面上
遇到这种问题,第一件事不是改代码,而是把现场信息补齐。我建议按这个顺序收集:
- 记录完整的启动堆栈,别只看第一行
NameNotFoundException。要把Caused by链路一并贴出来,确认异常抛出点是否确实在 Jersey 扫描器里。 - 把
web.xml全文拿出来,重点看ServletContainer的init-param里有没有com.sun.jersey.config.property.packages,参数值是什么。 - 确认当前项目的 Jersey 依赖版本。老项目喜欢用
jersey-bundle这种大杂烩依赖,版本跨度大,行为差异也大。 - 对比本地环境和测试环境的差异。如果本地能启动、测试环境不能,优先怀疑类加载器、JNDI 配置、服务器内核版本这些环境因素。
这些信息整理完,基本就能判断异常发生在应用启动的哪个阶段。如果堆栈指向PackageNamesScanner或PackagesResourceConfig,那问题就在包扫描阶段,和业务代码中的具体接口实现没有关系。
3.2 第二步:写一个显式注册的 Application 类做二分
为了验证“包扫描是罪魁祸首”,最快的办法是让 Jersey 暂时不要扫描,改用显式注册。先写一个最简单的Application子类,只注册一个资源类:
package com.example.rest; import javax.ws.rs.core.Application; import java.util.Collections; import java.util.Set; public class RestApplication extends Application { @Override public Set<Class<?>> getClasses() { return Collections.<Class<?>>singleton(HelloResource.class); } }然后把web.xml里的包扫描参数去掉,换成javax.ws.rs.Application参数:
<init-param> <param-name>javax.ws.rs.Application</param-name> <param-value>com.example.rest.RestApplication</param-value> </init-param>重新部署后再看启动日志。如果服务正常起来,没有任何NameNotFoundException,那基本坐实了问题出在 Jersey 的包扫描机制,而非HelloResource本身的代码问题。这一步非常关键,它把问题范围从“整个应用”缩小到“Jersey 扫描器和容器类加载器的配合”。
当然,这个RestApplication只是用来做定位的临时探针。项目里如果有很多资源类,不可能只注册一个。定位完成后,要么把它扩展成完整注册清单,要么再去研究如何让扫描器绕开容器的问题环节。
3.3 第三步:顺手验证 JNDI 环境确实没有 com 节点
如果你还想让根因更明确,可以在测试环境里做一个更直接的实验:在应用服务器管理控制台里查一下 JNDI 树根节点,或者写一个一次性 Servlet 执行下面的代码:
import javax.naming.InitialContext; import javax.naming.Context; Context ctx = new InitialContext(); Object obj = ctx.lookup("com.example.rest");如果这段代码抛出同样的NameNotFoundException: Name "com" not found,说明这个服务器的 JNDI 命名空间里确实不存在com节点。这个实验结果和 Jersey 无关,纯粹证明环境行为。注意,这只是验证手段,不要真把这种代码留在业务系统里,也不要在没有把握的情况下直接在生产环境试。JNDI 根节点被任意读取通常不会破坏环境,但谨慎一点没有坏处。
4. 几种落地的修复方案和不推荐的做法
4.1 方案一:显式注册资源类,彻底摆脱扫描
这是我认为最稳的方案,也是最终让我解决问题的手段。既然包扫描会在特定服务器的类加载器上触发 JNDI 联动,那就干脆不让 Jersey 做扫描。把所有资源类和 Provider 类手动列进Application子类:
package com.example.rest; import javax.ws.rs.core.Application; import java.util.Arrays; import java.util.HashSet; import java.util.Set; public class RestApplication extends Application { private static final Set<Class<?>> CLASSES = new HashSet<Class<?>>(Arrays.asList( HelloResource.class, UserResource.class, ReportResource.class, GlobalExceptionMapper.class )); @Override public Set<Class<?>> getClasses() { return CLASSES; } }这里的GlobalExceptionMapper是一个@Provider类。如果你有自定义的过滤器、拦截器或MessageBodyReader/Writer,也都要一并注册。漏掉一个,对应的功能就会在运行时失效,而且不像扫描失败那样启动时报错,可能是在调用某个接口时才出问题,排查起来更隐蔽。
这个方案的缺点也很明显:新增一个资源类后,必须手动改getClasses()。老项目如果资源类很多,很容易漏加。我的建议是配套写一个单元测试,在本地 JVM 里扫描目标包下所有带@Path的类,断言这些类都被RestApplication.CLASSES包含。这样本地不会触发服务器 JNDI,又能防止以后加接口时忘记注册。
4.2 方案二:保留包扫描,但替换 Jersey 的扫描器
如果你不想放弃自动扫描,理论上可以替换 Jersey 使用的扫描器。Jersey 1.x 在com.sun.jersey.core.spi.scanning包下有ScannerSPI,PackagesResourceConfig也支持传入自定义扫描器构造。大致思路是这样的:
public class ContainerSafeResourceConfig extends PackagesResourceConfig { public ContainerSafeResourceConfig() { super(new ContainerSafeScanner("com.example.rest")); } }ContainerSafeScanner需要自己实现scan(ScannerListener)方法,不要去调用容器类加载器的getResources,而是直接用File的方式遍历WEB-INF/classes目录,或者手动打开项目依赖中的所有 JAR,找到包路径对应的条目。这样就不会触发服务器类加载器的 JNDI 联动。
但这个方案有几个现实问题。第一,PackagesResourceConfig接收Scanner的构造器属于内部 API,版本不同可能存在差异,你得先确认自己用的 Jersey 1.x 小版本是否支持。第二,自己遍历WEB-INF/classes和 JAR 条目,需要处理各种边缘情况,比如多层级目录、JAR 没有目录条目、类文件缺失、Web 容器对目录访问的限制。第三,这个方案依然绑定 Jersey 1.x 的扫描 API,未来升级框架时所有自定义代码都要重新适配。
所以我对这个方案的态度是:作为技术探索可以,但除非有强烈的自动扫描需求,否则不建议长期维护这种方式。显式注册虽然“笨”,但胜在可控、可读、可测试。
4.3 方案三:在服务器 JNDI 树里补一个 com 节点(不推荐)
有一种绕过思路是在应用服务器的 JNDI 树根目录下手工绑定一个名为com的上下文节点。既然扫描器第一步要解析com,那com存在了,后续的example和rest如果也不需要真正被解析,扫描可能就能继续往下走。
我不推荐这个做法。原因有三点:
- JNDI 树是全服务器共享的,往根节点塞一个
com,会影响这个服务器上的所有应用,属于典型的全局污染。 - 这种绑定不具备可移植性。换一台服务器,或者服务器重启后配置丢失,问题立刻回来,而且比原来更难排查。
- 它只是掩盖了“类加载器被命名服务接管”的病根,并没有让 Jersey 避开这个耦合点。哪天包名改成
org.example.rest,你又要去绑定一个org节点。
这个方案只适合在极其特殊的情况下做临时应急,比如线上服务不能等代码重新发布,同时你确认加一个空节点不会影响其他应用。做完之后,后续还是要回到显式注册或迁移框架的正路上来。
下面是三个方案的简单对比,方便根据项目情况选:
| 方案 | 改动范围 | 维护成本 | 推荐度 |
|---|---|---|---|
| 显式注册资源类 | 应用代码 + web.xml | 较低,但需要维护清单 | 高 |
| 自定义扫描器 | 应用代码 + Jersey 内部 API | 较高,依赖框架实现 | 中 |
| 绑定 JNDI com 节点 | 服务器配置 | 低,但全局风险和后续隐患大 | 不推荐 |
5. 修改之后的验证、长期建议与我的个人偏好
5.1 修改后要验什么
方案落地后,不要只看“没有报错”就宣布解决。我一般会按这个清单做验证:
- 重新发布 WAR,查看启动日志中 Jersey 初始化的状态,确认没有
NameNotFoundException或ClassNotFoundException。 - 执行冒烟测试,把项目核心接口挨个调一遍,尤其是那些依赖自定义
@Provider的接口,确认手动注册没有漏掉关键类。 - 检查 WADL 或服务发现是否正常。部分老项目会开启
com.sun.jersey.config.feature.DisableWADL,如果切换为显式注册,这些配置项仍然生效,但要确认没有因为注册方式改变而影响运行时行为。 - 在新功能分支里新增一个资源类做验证,走一遍“新类会不会自动被发现”的工作流。如果你选择了显式注册,这一步会提醒团队以后需要改注册清单。
如果项目里还有别的模块用了com.sun.jersey.config.property.packages,记得一起改掉。只改了一个 servlet,其他 servlet 还在扫描的话,问题可能换个入口继续出现。
5.2 早点规划:从 Jersey 1.x 迁移出去
从根上解决这类问题的最好方式,其实是离开 Jersey 1.x。Jersey 1.x 最后的稳定版本停留在多年以前,包名是com.sun.jersey,大量接口属于旧 JAX-RS 1.x 时代的设计。它的扫描器、类加载器交互和现代 Servlet 容器之间已经有明显的“代沟”,类似这次 JNDI 联动的问题会随着服务器版本升级不断冒出来。
如果项目还有较长的维护期,建议做一个迁移评估。迁移到 Jersey 2.x 时,主要的变化包括:
- 依赖坐标从
com.sun.jersey体系换成org.glassfish.jersey体系。 - 核心导入包从
com.sun.jersey.api.core.ResourceConfig换成org.glassfish.jersey.server.ResourceConfig。 ServletContainer类路径从com.sun.jersey.spi.container.servlet.ServletContainer换成org.glassfish.jersey.servlet.ServletContainer。web.xml里的包扫描参数名从com.sun.jersey.config.property.packages换成jersey.config.server.provider.packages。- 注解方面,如果继续用 JAX-RS 2.x,
@Path、@GET、@Provider仍然在javax.ws.rs包下,改动不算伤筋动骨。
迁移工作量取决于项目对旧 Jersey 内部 API 的使用深度。如果只是用@Path写资源类,迁移成本不高;如果大量依赖ResourceFilterFactory、ContainerResponseFilter这类旧 API,就需要逐个替换。但只要项目还要长期运行,这笔技术债越早还越划算。
5.3 我在实践中的一点体会
经历过这次问题之后,我处理 Java 旧项目启动异常的顺序基本固定下来:先看初始化链路,再分环境对比,最后再决定改代码还是改配置。遇到“本地没事、服务器有事”的情况,我一般优先怀疑类加载器、JNDI、文件系统权限这类环境因素,而不是急着往业务代码里加判断。这次的NameNotFoundException就是一个典型例子,如果当时一味去查“com 为什么找不到类”,可能到现在还在绕圈子。
另外,遇到老项目里的自动扫描功能,我会多问一句:自动扫描带来的便利,是否值得承担容器兼容性的风险?在复杂部署环境下,显式注册资源类虽然要多维护一份清单,但它把所有不确定因素都关在门外。对于一个要上线到商用应用服务器的老系统来说,稳定和可预期远比“少写一行代码”重要。