尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

TongWeb报错cannot be cast to TypeBinding?JSP编译器与类加载器冲突排查实战

TongWeb报错cannot be cast to TypeBinding?JSP编译器与类加载器冲突排查实战 TongWeb7049m10 的部署日志里出现cannot be cast to com.tongweb.eclipse.jdt.internal.compiler.lookup.TypeBinding时很多人第一反应是去翻业务代码。我在几个生产项目里反复遇到过这个报错可以负责任地说绝大多数情况下问题不在你的业务逻辑而在类加载环境和 JSP 编译器之间。先解释两个关键点jdt是 Eclipse Java Development Tools 的缩写TongWeb 内置它来动态编译 JSPTypeBinding是编译器内部的“类型登记簿”负责记录和枚举各种 Java 类型。当 JVM 发现某个对象不是 TypeBinding却要强转成 TypeBinding 时就会抛出这个 ClassCastException。这篇内容主要写给遇到同样报错的开发、运维和实施同学从异常本身的含义讲起再按真实排障路径一步步定位最后给出可直接照做的解决方案。如果你在互联网上搜过这个问题会发现类似的报错在 Tomcat 里也有但包名略有不同。TongWeb 做的是com.tongweb.eclipse.jdt.*这套和我下面要讲的排障思路是相通的。括号里的lqw一般是环境标识、主机名或应用部署别名不用太纠结它不是异常类型的一部分。1. 拆解报错这个ClassCastException到底是谁抛出来的1.1 从异常文本提取三个有效线索这段异常里最有价值的是三部分。第一部分是cannot be cast to。它说明 JVM 在执行类型转换指令checkcast时发现栈顶对象的实际类型和期望转换的目标类型不一致。这不是业务代码手写(TypeBinding) obj导致的而是某个底层编译器或字节码处理框架在做内部转换时失败了。第二部分是com.tongweb.eclipse.jdt.internal.compiler.lookup.TypeBinding。这个类的完整包名暴露了一个信息TongWeb 内置的是经过改包处理的 Eclipse JDT 编译器。lookup包下的TypeBinding是编译器语义分析阶段的产物它代表“一个标识符最终绑定到了哪个类型”。比如在 JSP 页面里写了一个% user.getName() %JDT 在解析user这个变量时就要查表找到它的类型绑定这个过程中产生了不能转换的对象就会爆出这个异常。第三部分是jdt本身。Eclipse JDT 不只是 IDE 里的代码提示工具很多 Java 应用服务器的 JSP 编译能力都建立在它之上。Tomcat 的 Jasper 使用org.eclipse.jdt.internal.compiler.*TongWeb 则是在此基础上做了定制和改名形成com.tongweb.eclipse.jdt.*。用一个生活类比来说你手里拿着一张水电缴费单却想塞进身份证读卡器。虽然都是卡但读卡器只认身份证一试就报“类型不匹配”。ClassCastException 就是这个读卡器的提示。1.2 TongWeb 为什么会在运行时集成 JDT 编译器因为 JSP 是运行时编译的。JSP 本质上是一段模板文本第一次被请求时应用服务器要把这段文本转换成 Java 源码再用 Java 编译器编译成 class最后加载执行。这个“转换编译”的过程发生在运行期所以应用服务器肚子里必须藏着一套完整的 Java 编译器。TongWeb 选择改造 Eclipse JDT 是很聪明的做法。JDT 的编译器是纯 Java 实现能嵌入到 JVM 进程中不需要额外启动子进程也不会依赖操作系统的javac这样部署起来更稳定。但它也有一个副作用编译器内部逻辑存在大量 AST 节点和符号绑定对象之间的强转一旦外部环境破坏了类加载的一致性就会以这种“内部类转换失败”的形式爆炸。换句话说这个报错不是 TongWeb 独有的 bug而是“运行时编译器 类加载器冲突 字节码工具干扰”三类因素叠加后的典型症状。我把它归为中间件类故障而不是应用服务器的稳定性问题是因为同样的 war 包放到 Tomcat 里也可能出现类似cannot be cast to org.eclipse.jdt.internal.compiler.lookup.TypeBinding的异常只是暴露的包名不同。2. 哪些场景最容易踩中这个类型转换坑2.1 JSP 自动编译和动态增强工具同时抢戏最常见的触发场景之一是 JSP 自动重新编译和 Arthas 这类字节码增强工具同时在工作。如果你的应用开启了 JSP 自动重载第一次访问某个页面时TongWeb 会用内置 JDT 把 JSP 翻译成 Java 再编译成 class。而如果这时你正好开着 Arthas 对某个类做了watch或traceArthas 会通过字节码增强改写目标类的方法。两个操作同时落在同一个类加载器的加载链路上编译器内部对象在方法区查找时被增强逻辑干扰就有可能出现SingleNameReference cannot be cast to TypeBinding这种措辞的异常。我遇到过的一个现场是压测环境里只要开着 Arthas 的trace命令紧接着第一次访问 JSP 页面日志里就刷报错。把 Arthas 停掉重启应用再访问同样的 JSP一切正常。再开 Arthas 复现问题又回来了。这个复现路径基本确认了二者存在竞争关系。如果你也处于类似状态第一步不是改代码而是先停掉所有字节码增强工具清掉 work 目录再重启访问一次。如果异常消失说明问题就出在增强工具和运行时编译的叠加。2.2 应用自带的编译相关 jar 污染 classpath第二种场景更像“慢性病”它不挑时机可能启动没多久就炸。很多业务系统为了做规则引擎、动态脚本、在线表达式计算会在 pom.xml 里引入 JDT 或 ECJ 相关依赖。比如org.eclipse.jdt:ecj、org.eclipse.jdt.core这类 jar。如果这个依赖被打进了 WAR 包的WEB-INF/lib或者打进了 Spring Boot fat jar 的BOOT-INF/lib部署到 TongWeb 后应用类加载器加载org.eclipse.jdt.internal.compiler.*时就会优先用自己带的这份。问题是TongWeb 内置 JSP 编译器用的类名是com.tongweb.eclipse.jdt.internal.compiler.*它和标准的org.eclipse.jdt.internal.compiler.*是两套不同的类。当服务器 JSP 编译代码通过某种方式引用到了应用自带的 JDT 类或者反过来应用代码引用到了服务器内置的 JDT 类时这两个“长得像但又不是同一个”的类就会碰撞。这个场景最典型的现象是报错堆栈一会儿显示org.eclipse.jdt.internal.compiler.ast.FieldReference一会儿又显示com.tongweb.eclipse.jdt.internal.compiler.lookup.TypeBinding。这说明两个命名空间被搅在了一起。2.3 类扫描器和序列化框架误伤了编译器内部类第三种场景是框架在启动阶段扫描字节码误把编译器内部类当成业务类处理了。Spring 的组件扫描、MyBatis 的 Mapper 解析、Hibernate 的实体扫描、某些动态代理框架的切面扫描底层都会用 ASM 或类似的字节码库去读 class 文件。如果扫描路径覆盖了com/tongweb/**或org/eclipse/jdt/**ASM 会尝试解析这些编译器内部类的结构。解析过程中某个框架内部逻辑可能希望读取到某种类型的节点结果拿到的是编译器存储的符号绑定强转时就抛异常。有个容易被忽略的细节类扫描并不一定从 classpath 下的 jar 开始有些框架会扫描“已加载的所有类”。如果框架在 TongWeb 初始化之前加载了内置 JDT 类后续再用 ASM 二次解析就会出现这种错位。我见过一个项目启动日志里反复出现cannot be cast to TypeBinding堆栈指向某个动态类生成框架。起初大家都以为是框架版本太老后来把扫描范围里的com.tongweb排除掉问题就消失了。归根结底是脚手架不该去碰编译器内部的东西。3. 完整排查路径三步定位异常根源3.1 拿到完整堆栈而不是只看第一行排查这类问题第一件事是把完整堆栈从日志里捞出来。下面是我在一个真实项目里截取的关键片段java.lang.ClassCastException: org.eclipse.jdt.internal.compiler.ast.SingleNameReference cannot be cast to com.tongweb.eclipse.jdt.internal.compiler.lookup.TypeBinding at org.eclipse.jdt.internal.compiler.parser.Parser.consumeToken(Parser.java:...) at org.eclipse.jdt.internal.compiler.parser.Parser.parse(Parser.java:...) at org.eclipse.jdt.internal.compiler.parser.Parser.parseCompilationUnit(Parser.java:...)这种堆栈指向的是 Parser 和 Compiler 内部说明问题发生在编译期。如果堆栈指向业务框架的类加载器、ASM ClassReader、CGLIB 生成类则说明是类扫描或字节码增强环节出了问题。你还要分清是“首次访问 JSP 才报”还是“启动阶段就报”。前者和 JSP 编译时机强相关优先排查 JDT 依赖冲突后者要重点看类扫描配置和全局增强工具。提示不要忽略Caused by那一行。很多情况主异常是业务框架包装过的Caused by里才是真正的 JDT 内部转换失败。3.2 使用 classloader 定位 TypeBinding 的加载来源“类是从哪个 jar 加载的”是核心问题。有 Arthas 环境的话直接执行classloader -r com/tongweb/eclipse/jdt/internal/compiler/lookup/TypeBinding这个命令会列出所有能加载该类的 ClassLoader以及对应的 jar 路径。如果结果里出现了两个不同来源比如一个来自 TongWeb 安装目录下的 lib一个来自应用的WEB-INF/lib就可以实锤重复依赖了。没有 Arthas 也不用慌可以在测试环境临时写一个接口或 Servlet 打印加载来源ClassLoader cl Thread.currentThread().getContextClassLoader(); Class? c cl.loadClass(com.tongweb.eclipse.jdt.internal.compiler.lookup.TypeBinding); System.out.println(c.getProtectionDomain().getCodeSource());看到codeSource指向应用自己的 jar 时基本可以断定是应用重复带入了 JDT 相关依赖。这个临时接口验证完要记得删掉别留在生产环境。3.3 核对 TongWeb 版本和 JDK 版本的匹配关系TongWeb7049m10 对 JDK 版本有明确适配范围不同 build 对应的内置 ECJ 版本也不同。我发现一个高频规律本地用 JDK 11 或 JDK 17 打出来的 class 文件部署到用 JDK 8 运行的服务器上JSP 一编译就容易出现各种编译器内部异常。原因是高版本 JDK 编译出的 class 文件会携带新版字节码属性低版本 ECJ 在解析这些属性时可能走不到正常逻辑内部符号表处理就会出错。检查时先确认java -version再确认你的构建产物目标版本。可以通过javap查看单个 class 文件的字节码版本javap -verbose 你的类名.class | grep major如果本地编译 major 版本是 61JDK 17而服务器运行环境是 JDK 8支持 major 52那么即便代码本身没有用到高级语法也会因为 class 文件格式差异带来潜在的编译器兼容风险。建议统一构建 JDK 和运行时 JDK或者至少把maven.compiler.source和target设置到服务器支持的版本。4. 五套可落地的修复方案4.1 彻底清掉应用里多余的 JDT/ECJ 依赖这是最直接、解决概率最高的方案。Maven 项目先运行依赖分析mvn dependency:tree -Dincludesorg.eclipse.jdt如果结果里出现org.eclipse.jdt:ecj、org.eclipse.jdt.core之类的 jar找到是谁带进来的然后排除。示例dependency groupIdcom.example/groupId artifactIdsome-rule-engine/artifactId version1.0.0/version exclusions exclusion groupIdorg.eclipse.jdt/groupId artifactIdecj/artifactId /exclusion /exclusions /dependencyGradle 对应的写法是implementation(com.example:some-rule-engine:1.0.0) { exclude group: org.eclipse.jdt, module: ecj }打完包之后再检查一遍jar tf your-webapp.war | grep -E (jdt|ecj|eclipse)如果 war 里仍然看到org/eclipse/jdt/的 class说明还有漏网的依赖继续往上排查。业务代码如果确实需要动态编译 Java 源码尽量不要把整套 JDT 塞进应用 classpath。JDK 自带的javax.tools.JavaCompiler可以完成绝大部分需求或者把编译能力下沉到 TongWeb 的公共扩展目录而不是和业务 jar 混在一起。4.2 调整类加载委托策略让父加载器优先如果应用不想动依赖还可以在 TongWeb 部署配置里调整类加载委托策略。在conf/tongweb.xml或部署描述对应的 Context 节点中将 Loader 的delegate设为trueContext path/app reloadablefalse Loader delegatetrue / /Context这样应用类加载器在加载org.eclipse.jdt和com.tongweb.eclipse.jdt下的类时会先查看父加载器是否已加载过相同名称的类。如果是直接复用父加载器中的类避免两套 JDT 并存。但这条方案要慎重。父优先模式也会影响其他公共库比如应用中用了新版 Apache Commons、日志实现或 JSON 库而服务器自带了较旧版本可能会引发NoSuchMethodError。所以修改后一定要做全量回归测试不能只跑一下登录流程就算完。4.3 把 JSP 预编译掉减少运行时动态编译如果报错和 JSP 自动编译强相关最省心的办法是直接绕开运行时编译。在构建期用 JSPC 或 Ant 任务把 JSP 预编译好部署后服务器直接加载编译产物不再走 JDT 动态编译从根上消除了编译器内部类被误用的可能。TongWeb 通常提供配套的 JSPC 命令行工具。大致流程使用 jspc 工具或 IDE 插件将 JSP 编译到WEB-INF/classes对应目录部署后设置reloadablefalse防止运行期检测到 JSP 变更触发重新编译清除 work 缓存目录确保使用的是预编译产物。这个方案对“JSP 数量多、变更频繁”的项目不太友好因为每次改页面都要重新构建。如果你的项目页面很稳定或者正处于上线交付阶段预编译是一个很可靠的规避手段。4.4 排除类扫描范围避免框架“扫错人”如果异常来自 Spring 或某些自动扫描框架要在扫描配置中排除服务器内部包。Spring 为例ComponentScan(basePackages com.example, excludeFilters ComponentScan.Filter( type FilterType.REGEX, pattern com\\.tongweb\\..*|org\\.eclipse\\.jdt\\..*))MyBatis 的typeAliasesPackage、JPA 的packagesToScan、以及各类动态代理框架的include参数都要注意别把com.tongweb和org.eclipse.jdt扫进去。还有一类容易忽略的定时任务框架和监控组件里的“全 classpath 扫描”逻辑。我曾经排查过一个项目报错堆栈来自某个监控 SDK它在启动时遍历所有已加载类做元数据采集恰好扫到了 TongWeb 内置 JDT 的类。排除了com.tongweb后监控和业务都不再报错。4.5 统一 TongWeb 内置 JDT 版本与应用所需版本如果应用确实必须用 JDT比如你正在做一个在线 IDE 或者规则引擎那就不要和服务器“各玩各的”。先把 TongWeb 安装目录下lib或者modules里 JDT 相关 jar 的版本号记录下来把应用侧引入的 JDT 版本调成一致再放到服务器的公共类加载路径里。具体做法是把应用里的ecj相关依赖排除掉在 TongWeb 的lib或modules目录中确认已存在对应版本。如果服务器内置版本过低需要和你使用的 TongWeb 发行版本匹配必要时联系官方服务支持确认补丁版本而不是自己去网上下一个高版本 ECJ 替换进去。注意千万不要图省事直接修改 TongWeb 安装目录下的内置 jar尤其是替换 JDT 相关的 jar。这会让多个应用共享同一个被改过的编译器风险极大。除非有官方技术支持确认否则这种操作容易把其他应用也带崩。5. 生产环境避坑清单与案例复盘5.1 一个完整的现场案例复盘我处理过的一个真实案例某业务系统做中间件适配部署环境是 TongWeb7049m10 JDK 8应用是一个传统 WAR 包。启动顺畅但第一次打开登录页日志立刻出现异常java.lang.ClassCastException: org.eclipse.jdt.internal.compiler.ast.FieldReference cannot be cast to com.tongweb.eclipse.jdt.internal.compiler.lookup.TypeBinding at com.tongweb.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope...拿到堆栈后先做两件事查 WAR 包里的 jar再看依赖树。结果发现WEB-INF/lib里躺着一个ecj-3.7.jar是一个规则引擎组件带进来的。虽然业务代码没有直接 import 它但应用类加载器在加载org.eclipse.jdt.internal.compiler.ast.*时优先用到了这个老 jar。老 jar 里的内部结构和 TongWeb 内置 JDT 不一致最终在 JSP 编译换型时失败。解决方案就三步排除依赖重新打包清 work 目录。重启后访问页面异常消失。这个案例的核心不是代码而是classpath 中多出了一份“不该存在的编译器”。5.2 清空 work 缓存和重启的细节排障过程中清空 work 目录一定要做。TongWeb 会把 JSP 编译产物缓存在 work 目录下例如安装目录/work/应用名或domains/domain1/work/应用名。如果你改了依赖却不清理这些缓存重启后 TongWeb 可能直接使用旧的已编译 class问题不会消失反而会误导你继续排查。我习惯在每次修复后按固定顺序操作停应用删除 work 目录下对应该应用的缓存确认WEB-INF/lib或BOOT-INF/lib里已经没有 JDT 相关 jar启动应用并首次访问 JSP 页面验证。这个流程虽然简单但能避免大量“改了半天发现没生效”的假象。5.3 用 Arthas 排查时的二次风险Arthas 的watch、trace、redefine等命令本质上还是字节码增强。如果事故已经和 JDT 编译器有关你再对相关类做动态增强可能叠加出新的异常。建议排查顺序是先看堆栈和 classpath不要一上来就开增强命令。确认没有重复依赖之后再考虑用 Arthas 查看运行时的对象关系。如果必须用优先用静态查询命令比如classloader、sc、jad少用会改写字节码的watch、trace。5.4 高频问题速查表问题现象可能原因解决方向JSP 首次访问报 TypeBinding 转换异常应用带了旧版 ecj/jdt jar排查并移除重复依赖启动过程报错堆栈指向业务框架类扫描/代理框架误扫服务器内部类排除 com.tongweb、org.eclipse.jdt 包压测或调优时偶发伴随 Arthas 等工具字节码增强和 JSP 编译并发冲突先停增强工具清 work 目录重启验证本地 JDK 高版本打包服务器 JDK 低版本运行字节码版本与内置 ECJ 不兼容统一编译目标和运行 JDK 版本更换依赖后问题依旧work 缓存旧 class 未清理删除 work 目录后重启回到标题里的那个报错。我在 TongWeb7049m10 上几次遇到cannot be cast to com.tongweb.eclipse.jdt.internal.compiler.lookup.TypeBinding最终定位到的原因无一例外都和“额外引入的 JDT 依赖”或“类扫描越界”有关。过程中最让我觉得值得记录的一点是不要被一大段编译器内部堆栈吓住也不要急着给服务器换版本、换补丁先静下心来看 classloader 和依赖树问题往往很快清晰。这个排查套路用在 Tomcat、WebLogic 等同样内置 JSP 编译器的中间件上也是大同小异。如果你手头正遇到这个报错按我上面说的三层排查路径走一遍大概率能在半小时内找到真正的引入者。
返回列表