ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TongWeb部署老项目遇ClassNotFoundException:Jersey包名迁移与类加载冲突

TongWeb部署老项目遇ClassNotFoundException:Jersey包名迁移与类加载冲突 “Caused by: java.lang.ClassNotFoundException: com.sun.jersey.spi.inject.InjectableProvider”看到这个堆栈的时候我第一反应不是去翻代码而是叹了口气——又是老旧业务系统迁移到国产中间件时的经典“历史账”。最近在帮一个做政务数据交换平台的老客户处理 TongWeb 部署问题他们一个跑了好几年的 Spring Boot 1.5.x 老服务从 Tomcat 挪到 TongWeb 7.0 上启动到一半就抛这个异常直接导致接口全部 503。这种问题最折磨人的地方在于明明在 Tomcat 下一切都正常代码一行没改换个容器就炸锅。而且报的类名com.sun.jersey.spi.inject.InjectableProvider属于 Jersey 1.x 时代的产物如今大部分开发根本没听过这个类名。它牵涉到的不仅仅是缺失一个 jar 那么简单背后是 JAX-RS 实现分裂史、Maven 依赖冲突、中间件类加载隔离机制三个层面的叠加。如果你也在 TongWeb或者 WebLogic、Resin 这类商业容器上部署老项目遇到同款事件或者启动时闪退、隔一阵子就内存溢出这篇文章就是给你写的。我会把从“看到报错”到“彻底解决”的完整链路拆给你看每一步为什么这么做、底层发生了什么都会交代清楚。1. 先把这个异常类查清楚Jersey 1.x 与 2.x 的“包名分家”1.1 从 JAX-RS 规范说起JAX-RSJava API for RESTful Web Services是一套 Java 里定义 REST 接口的规范定义了一堆注解比如Path、GET、POST等等。但规范是指导性文件真正干活的是具体实现。业界有两个最出名的实现JerseyJAX-RS 的参考实现由 Oracle/Sun 主导最初包名是com.sun.jersey。Restlet、CXF另外的第三方实现也遵循 JAX-RS 规范。你的项目里报com.sun.jersey.spi.inject.InjectableProvider说明应用在启动时有组件需要一个 Jersey 1.x 的 SPI 扩展接口。什么叫 SPI就是 Service Provider Interface服务提供方暴露给外部扩展的插槽。InjectableProvider这个接口的作用是让开发者把自己自定义的Injectable实例注册进 Jersey 的依赖注入体系里一般在做自定义参数解析或过滤器时才会碰到。1.2 为什么包名突然变了Sun 被 Oracle 收购后Jersey 项目经过了一段重构。2013 年前后Jersey 2.x 正式发布包名从com.sun.jersey.*整体迁移到了org.glassfish.jersey.*。Jersey 2.x 基于 JAX-RS 2.0 规范加入了大量新特性比如异步响应、过滤器和拦截器的标准化同时底层依赖注入从自研机制换成了 HK2一个类似 Spring 的轻量级 IoC 容器。所以com.sun.jersey.spi.inject.InjectableProvider只在老版本 Jersey 1.x 里存在对应新版本的是org.glassfish.jersey.spi.inject.InjectableProvider。这里就是第一个“坑”的温床——很多老项目的 pom 里可能从某个祖传依赖里传递引入了jersey-core、jersey-server等 1.x 坐标自己根本不知道。这些 jar 里的核心类全是com.sun.jersey开头。平时在 Tomcat 里加载顺序刚好没冲突一切岁月静好一旦换了容器类加载顺序变了问题就暴露了。1.3 项目里为什么会有这种远古坐标我帮客户排查的时候发现他的 pom 文件里没有直接写任何 Jersey 依赖但mvn dependency:tree拉出来一看好家伙三个地方都在往里塞一个内部研发的旧 SDK基于 Jersey Client 封装的 HTTP 调用工具坐标是com.sun.jersey:jersey-client:1.19.4。一个老的 Apache Camel 组件里面引用了jersey-core。还有个 Spring Cloud 时代的遗留feign-jaxrs模块也绑了 Jersey 1.x。说白了这是典型的“传递依赖失控”。你只看到自己写了 100 行业务代码底下的基建框架却在各个犄角旮旯里藏着老版本 Jersey。对比项Jersey 1.xJersey 2.x包名com.sun.jersey.*org.glassfish.jersey.*SPL 接口com.sun.jersey.spi.inject.InjectableProviderorg.glassfish.jersey.spi.inject.InjectableProvider基于规范JAX-RS 1.xJAX-RS 2.0/2.1依赖注入Jersey 自研 SEI/SESPIHK2典型版本1.9、1.19.x2.25.x、2.35.x2. TongWeb 的类加载策略为什么在 Tomcat 没事换容器就崩2.1 Tomcat 的“委托优先但允许应用覆盖”Tomcat 的默认类加载模型是父委托优先Parent First但允许应用自己覆盖。它有一个WebappClassLoader默认会先看父加载器也就是common、shared等全局 lib能不能加载这个类找不到再去WEB-INF/lib里找。如果两边都有同一个类通常WEB-INF/lib里的优先因为 Tomcat 会先检查应用自身的类加载器再去父加载器这是它比严格双亲委托更灵活的地方。大多数情况下老项目里带了一堆com.sun.jersey:1.x的 jarTomcat 就直接加载了应用目录下的这些类跑得顺顺的。一些没人使用的老代码路径比如只注册未实际被调用的 Jersey 过滤器可能根本不会被触发反射自然就不会报错。2.2 TongWeb 的隔离与冲突逻辑TongWeb 作为国内商业化中间件类加载机制有自己的实现。它的默认配置更偏向严格的双亲委托或应用间高度隔离。具体来说TongWeb 启动每个 Web 应用时会创建一个WebClassLoader但它对domain/lib全局公共库和应用的WEB-INF/lib之间的优先级处理方式和 Tomcat 不一样。TongWeb 偏向“全局优先”也就是当domain/lib里有某个类的同名同包类时它会优先用全局的而不是应用自己带的。这里就是翻车现场。TongWeb 自带的lib/javaee目录里实际上封装了一部分 JAX-RS 相关实现基于 Jersey 2.x 或自研兼容层。应用里有一堆com.sun.jersey:1.x的类当 JVM 尝试加载com.sun.jersey.spi.inject.InjectableProvider时TongWeb 的类加载器可能去全局目录找发现没有这个 1.x 接口然后它回头去应用目录加载理论上也能加载到但问题是加载时机。报错出现在 Spring 容器初始化期间通常是ContextLoaderListener或者内嵌 Jersey 的过滤器做BeanFactory后置处理时触发了对某个实现了 1.xInjectableProvider接口的扫描。由于类加载器在“全局优先”策略下无法在其可见范围内找到该接口直接抛NoClassDefFoundError随后被封装为ClassNotFoundException。2.3 宏观层面的“三明治冲突”我用个生活化类比帮你理清这个问题。你把一个老项目比作一个团队团队里有老员工Jersey 1.x和新员工Spring 等接口。在 Tomcat 这个“老办公室”里老员工轻车熟路管理层也知道怎么安排。换到 TongWeb 这个“新办公室”老板TongWeb 全局加载器先按自己的花名册点名发现没有老员工的档案就去找档案室应用 lib结果档案室确实有但老板因为流程问题先在全局查了一圈没查到就直接判定“查无此人”老员工被卡在新旧办公室的信息孤岛里。这个场景我在好几家迁移到 TongWeb 的老系统上都见过绝对是一个高频问题。3. 从报错到根因的完整排查链路按操作顺序复现一遍排查这种问题一定不要直接改代码。要先建立证据链。我把当时的操作步骤完整写下来你可以直接照着敲。3.1 看懂 Caused by 链条先别急着修类完整的报错大概长这样我精简了匿名内部类org.springframework.beans.factory.BeanCreationException: Error creating bean with name resourceConfig... Caused by: java.lang.NoClassDefFoundError: com/sun/jersey/spi/inject/InjectableProvider at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) Caused by: java.lang.ClassNotFoundException: com.sun.jersey.spi.inject.InjectableProvider at java.net.URLClassLoader.findClass(URLClassLoader.java:382) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at com.tongweb.web.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1149)注意看第一层是NoClassDefFoundError这是“类加载失败”的包装第二层才是真正的ClassNotFoundException。这里的关键不是补一个类而是要搞清楚谁在没有这个类的情况下尝试定义它答案通常是延迟加载。某个 jersey 1.x 的jar在META-INF/services里声明了 SPI 实现Spring 或 HK2 的 ServiceLoader 机制扫描到这个声明后尝试反射实例化结果类加载器找不到对应接口GG。所以第一步打开WEB-INF/lib列出所有包含com/sun/jersey开头的 jar并记录下来。cd /path/to/tongweb/webapps/your-app/WEB-INF/lib ls -la | grep -i jersey # 结果示例 # jersey-client-1.19.4.jar # jersey-core-1.19.4.jar # jersey-server-1.19.4.jar # jersey-servlet-1.19.4.jar同时也要检查全局 libcd /path/to/tongweb/domain/lib ls -la | grep -i jaxrs # 结果示例 # tongweb.jaxrs.jar # jersey-common-2.25.1.jar # jersey-client-2.25.1.jar3.2 Maven 依赖分析把所有 Jersey 坐标揪出来如果你的服务是基于 Maven 构建的这一步几乎是必须的。用dependency:tree看传递依赖指定-Dverbose才能看到是哪个路径引入的。mvn dependency:tree -Dverbose -Dincludescom.sun.jersey mvn dependency:tree -Dverbose -Dincludesorg.glassfish.jersey输出里你会看到类似[INFO] - com.xxx:data-sdk:1.5.2:provided [INFO] | \- com.sun.jersey:jersey-client:jar:1.19.4:compile [INFO] - org.apache.camel:camel-cxf:2.0.0:compile [INFO] | \- com.sun.jersey:jersey-core:jar:1.8:compile [INFO] \- com.sun.jersey:jersey-server:jar:1.19.4:compile-Dincludes可以只过滤 Jersey 相关的节点省得被几千行 tree 刷屏。注意同一项目里可能出现1.8 和 1.19.4 两个版本共存这本身就是隐患。到了这一步你就很清楚了项目引入了至少两个底层 jar>jar tf /path/to/jersey-core-1.19.4.jar | grep InjectableProvider jar tf /path/to/jersey-server-1.19.4.jar | grep InjectableProvider如果不确定哪个 jar 有可以先全量 grepfind . -name *.jar -exec sh -c echo $1; jar tf $1 | grep -l InjectableProvider echo $1 包含 _ {} \;注意在 JDK 8 里javax.ws.rs和com.sun.jersey的接口位于不同的 jar。而 TongWeb 自带的全局 JAX-RS 是基于Jersey 2.x 包名的根本不可能提供com.sun.jersey.spi.inject.InjectableProvider。所以此刻“缺失类”的真相是应用里有部分 jar 引用了这个接口但接口所在的那个 jar 没有被加载到应用类加载器里或者被全局加载器的同名类遮蔽了。我们在客户现场实际查到的情况是WEB-INF/lib/jersey-server-1.19.4.jar里确实有com.sun.jersey.spi.inject.InjectableProvider的.class文件但 TongWeb 默认开启了useParentClassLoadertrue加上我们部署时把tongweb.jaxrs.jar放在了应用的WEB-INF/lib下这是个错误操作导致 JVM 对一个同名类加载了两遍其中一个被标记为无效。3.4 动态验证用 jcmd 和 HSDB 确认类加载来源如果你需要进一步确认某个类是被哪个类加载器加载的可以用 JDK 自带的jcmd# 先找到运行中的 Java 进程 PID jps -l # 查看某个类的加载情况需要 JDK 8 jcmd PID VM.command_line # 用 HSDB 或 jhsdb 进行深入分析可视化操作命令行分析类加载信息比较晦涩多数时候我用不到这一步但如果你排查到怀疑是类加载顺序时可以用-verbose:class重启应用直接看类的加载时间和加载器# 在 TongWeb 的 domain 的启动脚本里追加 JAVA_OPTS$JAVA_OPTS -verbose:class然后筛选grep jersey /path/to/tongweb/logs/tongweb.stdout.log | grep InjectableProvider输出会显示类是来自file:/.../TongWeb/domain/lib/jersey-common-2.25.1.jar还是来自file:/.../webapps/your-app/WEB-INF/lib/jersey-server-1.19.4.jar。这一看就定案了。4. 三种可行的修复方案以及各自的“坑”拿到证据链之后剩下就是选择修复策略。我给出三种实际验证过能跑通的方案各有使用场景别盲目用第一个。4.1 方案一统一升级到 Jersey 2.x并清理调用代码一劳永逸如果你的项目里直接使用了 Jersey Client 或 Restful 服务端注解建议直接升级到 Jersey 2.x。拿客户的老项目举例它的com.sun.jersey包装类基本只有Client.create()和WebResource。改动要点修改pom.xml里的依赖!-- 删除旧依赖 -- !-- dependency groupIdcom.sun.jersey/groupId artifactIdjersey-client/artifactId version1.19.4/version /dependency -- !-- 换成新的 -- dependency groupIdorg.glassfish.jersey.core/groupId artifactIdjersey-client/artifactId version2.25.1/version /dependency dependency groupIdorg.glassfish.jersey.inject/groupId artifactIdjersey-hk2/artifactId version2.25.1/version /dependency注意Jersey 2.x 时代必须显式引入 HK2 依赖注入模块否则也会报Unable to find a provider for injection。然后改代码// 旧写法 com.sun.jersey.api.client.Client client com.sun.jersey.api.client.Client.create(); WebResource resource client.resource(http://your-api/endpoint); // 新写法 javax.ws.rs.client.Client client javax.ws.rs.client.ClientBuilder.newClient(); javax.ws.rs.client.WebTarget target client.target(http://your-api/endpoint);这个方案适合代码量不大、且对 Jersey 的依赖很表面的情况。坑在于如果你底层框架比如某个内部 SDK用了 Jersey 1.x 的内部实现类如com.sun.jersey.api.client.filter.HTTPBasicAuthFilter那升级后这些类不存在需要同步替换为 2.x 对应的org.glassfish.jersey.client.filter.HttpBasicAuthFilter。得全局搜索一遍。4.2 方案二精准排除传递依赖保留 1.x 但只留一份最常用如果项目里对 Jersey 1.x 的依赖深到骨子里比如有个历史遗留 SDK 就是基于它的ClientAPI 写的短期不建议全量升级。此时要做的是应用内只留一套完整、统一的 Jersey 1.x并移除其它冲突坐标。结合我们的问题重点是保证jersey-server-1.19.4.jar和jersey-core-1.19.4.jar同时存在于WEB-INF/lib且只有一个版本。在pom.xml里把其它引入的传递依赖排除掉dependency groupIdcom.xxx/groupId artifactIddata-sdk/artifactId version1.5.2/version exclusions exclusion groupIdcom.sun.jersey/groupId artifactIdjersey-client/artifactId /exclusion exclusion groupIdcom.sun.jersey/groupId artifactIdjersey-core/artifactId /exclusion /exclusions /dependency dependency groupIdorg.apache.camel/groupId artifactIdcamel-cxf/artifactId version2.0.0/version exclusions exclusion groupIdcom.sun.jersey/groupId artifactIdjersey-core/artifactId /exclusion /exclusions /dependency然后显式引入唯一版本dependency groupIdcom.sun.jersey/groupId artifactIdjersey-client/artifactId version1.19.4/version /dependency dependency groupIdcom.sun.jersey/groupId artifactIdjersey-server/artifactId version1.19.4/version /dependency这个方案的核心思路让 Maven 仲裁最终只产出一个 1.19.4 的完整集合。但注意camel-cxf:2.0.0这种老组件它的 class 字节码是基于jersey-core:1.8编译的有时候强行用 1.19.4 在运行时某个方法不存在或行为变化会导致新的AbstractMethodError。所以做完这步必须对 camel 相关路由做一次冒烟测试。4.3 方案三调整 TongWeb 类加载模式与 lib 目录绕不开的中间件配置TongWeb 的类加载策略是可配置的。在domain/domain.xml或 admin 控制台找到对应应用或者全局的 class-loader 配置。domain application nameyour-app ... resources context-param param-nameweblogic-classloader-useParent-class-first/param-name param-valuefalse/param-value /context-param /resources /application /domain严格来说TongWeb 的配置字段名可能是classLoaderMode或useChildFirstClassLoader。在 TongWeb 7 的 web 管理后台里路径是“应用管理 - 部署应用 - 选择应用 - 类加载模式”下拉选项一般就是“父类委托优先Parent First”和“应用优先Child First / Parent Last”。对我们这个案例必须选择“应用优先Parent Last”让应用自己的WEB-INF/lib中的类先于全局 lib 加载这样才能保证com.sun.jersey.spi.inject.InjectableProvider被正确加载。同时一个常见的错误做法就是把tongweb.jaxrs.jar或任何jersey-*.jar复制进应用的WEB-INF/lib。这会导致跟全局 jar 出现“同名双类”问题反而更乱。正确做法应用 lib 中只保留com.sun.jersey相关 jar全局模块TongWeb lib 或 modules 目录里的org.glassfish.jerseyjar 保持原样不去动它。配置完成后重启 TongWeb 的 domain清扫问题。这种方式适合不能改代码的场景但从根因上看治标不治本全局加载器里依旧有 Jersey 2.x应用里是 1.x一旦代码里出现跨加载器引用同名类如打包了javax.ws.rs.ext.Provider接口还是会触发另一个坑。4.4 选型逻辑什么时候用哪个我给客户最终的建议是组合拳短期快速恢复直接切“应用优先”类加载模式 清理WEB-INF/lib里的冗余 jar让服务先跑起来。中期根治用方案二把整个应用依赖树清理一遍确保每个 jar 只保留一个版本。长期优化若有重构机会把内部的 SDK 逐步替换成 Spring RestTemplate 或 Apache HttpClient彻底摆脱 Jersey 1.x 这个老包袱。方案适合场景改动量风险方案一升级 2.x代码对 Jersey API 使用较轻中等API 不兼容需改代码方案二清理依赖依赖较深无法快速升版较小老组件版本兼容性风险方案三改中间件配置不能改代码需快速恢复几乎为零治标不治本且全局/应用类交叉问题仍在5. 顺带解决“tongweb 内存溢出”类加载冲突如何伪装成频繁 Full GC很多人搜这个问题时会关联到“tongweb 内存溢出”这并非偶然。上面这种类冲突如果不彻底处理长时间运行后通常伴随 Metaspace 溢出或老年代 Full GC 越来越频繁最终服务夯死。5.1 Metaspace 泄漏的成因反复部署与类加载器不释放当你的应用在 TongWeb 中反复部署、重载比如修改了WEB-INF/lib里的 jar 后热更新应用类加载器实例每次都会新建。如果这个类加载器引用的一些类无法被卸载比如持有了 Classloader 的引用、线程、连接池Metaspace 的占用就会只增不减。在类冲突场景里这个老项目在启动初期就报错了但 TongWeb 往往不会直接终止进程而是把WEB-INF/lib里的多个 Jersey jar 都加载了一部分导致同一个类被多个加载器各加载一遍。这些“重复类”占据大量 Metaspace加上频繁热部署几个小时后java.lang.OutOfMemoryError: Metaspace就出现了。5.2 用 jstat 和 MAT 确认是 Metaspace 问题还是业务创建了大量对象需要区分一下“内存溢出”是堆溢出还是 Metaspace 溢出。我一般先看 JVM 参数和监控# 查启动参数里 Metaspace 的大小 jcmd PID VM.flags | grep Metaspace # 实时查看 Metaspace 使用率 jstat -gc PID 1000 10输出重点关注MUMetaspace 已用、MCMN/MCMX等值。如果MU持续增长且接近MCMX基本确认是类加载泄漏。然后抓一次堆转储jcmd PID GC.heap_dump /tmp/heap.hprof用 MAT 打开heap.hprof在Histogram里搜Metaspace或者ClassLoader查看有没有大量重复的类。不过更直观的是看jmap -clstats PID输出它直接给你列出每个类加载器加载了多少类。5.3 部署前的预防措施JVM 参数与依赖清单预防这个问题的核心是“不让类重复/不让加载器残留”。我的建议调整MaxMetaspaceSize。虽然加大参数不能根治泄漏但能拉长“崩溃前”的时间窗口配合监控去处理根因JAVA_OPTS-XX:MaxMetaspaceSize512m -XX:PrintGCDetails -XX:PrintGCDateStamps在部署前用mvn dependency:analyze检查依赖是否冗余特别关注 compile 与 runtime 作用域的重复 jar。不要一股脑往WEB-INF/lib塞 jar尽量规范构建用maven-war-plugin控制打包体积。TongWeb 控制台上关闭“部署时自动扫描所有 jar 的 SPI 接口”这类选项如果存在减少无效类加载。在客户现场我们做完类加载模式调整和依赖清理后Metaspace 从 300M 一路回落到稳定在 150M 左右Full GC 频率降了一个数量级问题才算真正画上句号。最后说两句经验这种ClassNotFoundException: com.sun.jersey.spi.inject.InjectableProvider本质上不是缺一个类而是依赖管理失控加容器类加载策略变化共同作用的结果。处理老系统迁移到 TongWeb 这类商业中间件时记住三个原则先查类加载器再清依赖树最后才动代码。另外每次换中间件前养成跑一遍mvn dependency:tree和列出WEB-INF/lib清单的习惯能为你省下几个通宵的排查时间。
返回列表