ARTICLE DETAIL

资讯详情

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

TongWeb中间件TypeBinding异常排查与解决方案

TongWeb中间件TypeBinding异常排查与解决方案 先还原一下我当时遇到这个报错的场景测试环境发版之后同事反馈某个页面直接 500后台日志刷出来一行让人摸不着头脑的异常——cannot be cast to com.tongweb.eclipse.jdt.internal.compiler.lookup.TypeBinding。第一反应是业务代码里某个对象被强转错了结果翻了一圈 Service、Dao压根找不到任何和TypeBinding相关的影子。这个类是 TongWeb 中间件自带的属于 Eclipse JDT 编译器内部的东西和业务代码八竿子打不着。当时就意识到这大概率是 TongWeb 应用服务器自身在 JSP 编译环节出了问题而不是普通应用代码 bug。这篇文章就把我当时从报错、排查到解决的完整过程整理出来给后面在东方通 TongWeb尤其是 7.0.4.9 M10 这个版本段上跑 Java Web 应用的同学做个参考。1. 报错现场还原这个 TypeBinding 异常到底长什么样1.1 完整堆栈不只是“一行”的问题很多人一看到ClassCastException就习惯性只去看报错那一行比如java.lang.ClassCastException: com.tongweb.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding cannot be cast to com.tongweb.eclipse.jdt.internal.compiler.lookup.TypeBinding但实际上这种 ClassCastException 真正的有效信息往往在下面的调用栈里。我那次抓到的完整堆栈大致是这样的关键帧做了脱敏处理java.lang.ClassCastException: com.tongweb.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding cannot be cast to com.tongweb.eclipse.jdt.internal.compiler.lookup.TypeBinding at com.tongweb.eclipse.jdt.internal.compiler.lookup.ClassScope.findSuperType(ClassScope.java) at com.tongweb.eclipse.jdt.internal.compiler.lookup.ClassScope.connectTypeHierarchy(ClassScope.java) at com.tongweb.eclipse.jdt.internal.compiler.lookup.ClassScope.buildTypeBindings(ClassScope.java) at com.tongweb.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration.bind(AbstractMethodDeclaration.java) at com.tongweb.eclipse.jdt.internal.compiler.ast.TypeDeclaration.internalGenerateCode(TypeDeclaration.java) at com.tongweb.eclipse.jdt.internal.compiler.ast.TypeDeclaration.generateCode(TypeDeclaration.java) at com.tongweb.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration.dietParse(CompilationUnitDeclaration.java) at com.tongweb.eclipse.jdt.internal.compiler.Compiler.parse(Compiler.java) at com.tongweb.eclipse.jdt.internal.compiler.Compiler.process(Compiler.java) at com.tongweb.eclipse.jdt.internal.compiler.Compiler.compile(Compiler.java) at com.tongweb.engine.jsp.compiler.JspCompiler.compile(JspCompiler.java) at com.tongweb.engine.jsp.servlet.JspServletWrapper.service(JspServletWrapper.java)从堆栈能明显看出几个关键信息报错发生在ClassScope.findSuperType阶段这是 JDT 编译器在做类型绑定resolve type binding时的一个内部步骤。堆栈最底层是JspServletWrapper.service说明是 JSP 请求触发的编译。也就是说这个异常不是在你写的业务代码里发生的而是 TongWeb 的 JSP 编译器在尝试解析某个 JSP 页面里的类型继承关系时从内存缓存里取出了一个“不对版”的类型对象。1.2 这类报错的共同特征把网上社区、群里反馈以及我自己的复现情况凑到一起这类cannot be cast to TypeBinding的报错基本都满足以下一个或多个触发条件触发场景具体表现为什么容易中招发布新版本后首次访问 JSP页面 500重启后可能恢复旧的编译缓存和服务端新加载的类不匹配多个请求同时访问同一个 JSP并发触发同一 JSP 编译JDT 编译器内部对同一编译单元并发处理不友好JSP 文件在编译窗口期被修改编译到一半文件变了类型绑定的“快照”和磁盘现状不一致应用目录或 work 目录残留旧版本文件页面时好时坏旧的 .class 和新的 .java 混在一起被编译器扫到应用 lib 内放入了和容器冲突的 JSP/JDT 相关包稳定复现类加载器里出现了多份同路径但不同来源的类我那次的情况很典型同事直接在生产 TomcatTongWeb的 webapps 下把 war 包解压覆盖没删掉上一层级的旧目录文件然后马上重新访问 JSP。第一次访问没问题第二次访问就开始报这个错。这基本可以锁定是“旧编译缓存 新页面内容”之间的状态错乱。2. 为什么偏偏是 TongWeb 报这个错JDT 编译器在中间件里的角色2.1 JSP 要经过“三步加工”才能变成 Servlet要理解这个报错得先把 JSP 的“变身”过程理清楚。一个.jsp文件在第一次被访问时TongWeb其实 Tomcat 家族都一样会按这个流程处理JSP 转 Java把 JSP 里的 HTML、标签、脚本片段翻译成一个_jsp.java文件。Java 编译成 Class用 Java 编译器把_jsp.java编译成_jsp.class。加载并执行ClassLoader 加载这个 Class实例化为 Servlet然后执行_jspService方法。其中第二步的“Java 编译器”在 Tomcat 系应用服务器里默认并不是我们平时用的javac而是Eclipse JDTEclipse Java Development Tools里的编译器实现。TongWeb 作为以 Tomcat 内核为基础的国产化中间件自然也沿用了这条技术路线只不过为了品牌化和避免包名冲突把org.eclipse.jdt改成了com.tongweb.eclipse.jdt——所以你才会在包名路径里看到com.tongweb.eclipse.jdt.internal.compiler.lookup.TypeBinding这种写着“TongWeb 定制”字样的内部类。2.2 为什么用 JDT 而不用 javacJDT 是一个纯 Java 实现的 Java 编译器它有几个非常适合做中间件内置编译器的特点不依赖外部 JDK 的 tools.jar只要 JVM 能跑它就能跑。对中间件来说部署环境可以不安装完整 JDK一个 JRE 就够。支持在运行时动态编译编译动作可以发生在内存里不用频繁读写临时文件尽管 JSP 编译还是要落盘生成_jsp.java和_jsp.class。支持增量编译和缓存机制可以在内存里保留已编译的 AST抽象语法树和类型绑定结果下次编译同一类文件时提速。这套机制在正常情况下非常稳而且比调用外部 javac 进程快很多。但问题是JSP 编译是一个有状态的过程——JDT 会在内存里缓存类型绑定的结果、类层级结构、AST 节点等数据。一旦这个缓存状态和应用实际加载的 class 文件不一致就会出现TypeBinding转换失败这类“内部错误”。2.3TypeBinding到底是个什么东西TypeBinding是 JDT 编译器内部用来表示“一个 Java 类型在编译期是什么样”的核心对象。你可以把它想象成编译器的一张“类型户口卡”每个变量、每个方法参数、每个 class 引用在编译时都要查到对应类型的户口卡才能知道这个类型有哪些方法、继承了什么父类、实现了哪些接口。JDT 里有很多类型的 Binding比如SourceTypeBinding源码里定义的类对应的绑定。BinaryTypeBinding编译后的 class 文件对应的绑定。MethodBinding方法绑定。FieldBinding字段绑定。它们大多继承自TypeBinding这个基础类。正常情况下编译器根据上下文能拿到正确类型的绑定对象。但如果缓存的元数据错乱了编译器预期拿到一个TypeBinding实际拿到的却是另一个不兼容的类型对象就会抛ClassCastException: XxxBinding cannot be cast to TypeBinding。拿生活场景打个比方你去户籍窗口查“张三”的信息窗口工作人员拿出来的却是“李四”的户口本系统就报错了。知识本身没问题问题是按错的编号拿错了本子。3. 排查链路从堆栈到根因的完整过程3.1 第一步把完整堆栈抓到本地先别盯着第一行排查这种问题最忌讳的一步是看到 ClassCastException 就去业务代码里找强转。第一件事应该是把完整堆栈从应用服务器日志里导出。TongWeb 默认日志目录一般在中途logs下和 Tomcat 类似catalina.out、localhost.log这类文件里会有完整调用栈。导出之后看堆栈的底部at com.tongweb.engine.jsp.compiler.JspCompiler.compile(JspCompiler.java) at com.tongweb.engine.jsp.servlet.JspServletWrapper.service(JspServletWrapper.java)这已经明确告诉你问题出在JSP 编译链路和业务代码没关系。如果堆栈底部是java.lang.Thread.run或者你自己的类那才需要考虑业务代码层面的问题。3.2 第二步先做一次“无脑操作”——清理 work 目录我调试中间件问题有一个习惯看到 JSP 相关异常第一把先清 work 目录。因为 TongWeb 会把 JSP 编译生成的_jsp.java和_jsp.class都放在work/Catalina/localhost/应用名/这个目录下。当时的操作流程是# 先把应用对应的实例停掉 cd /opt/tongweb/bin ./shutdown.sh # 清理 work 目录下对应应用的编译产物 rm -rf /opt/tongweb/work/Catalina/localhost/你的应用名 # 重新启动 ./startup.sh执行完之后让同事重新触发那个 JSP 页面报错消失了。这并不是玄学原因很简单JSP 第一次被访问时会重新生成 java 源码、重新编译。work 目录里的旧编译产物被清掉之后JDT 编译器等于从零开始不再受旧缓存的干扰。提示清理 work 目录会导致所有 JSP 页面在清理后的第一次访问时重新编译一次第一次访问的耗时可能会从几十毫秒变成几百毫秒但这通常是可以接受的代价。3.3 第三步确认是不是热部署触发的“状态错乱”如果清理 work 目录后问题不再出现那基本确认是缓存/状态错乱。但接下来要搞清楚一个问题为什么会错乱否则下一次发布还会踩同一个坑。我当时的排查方式是看 TongWeb 控制台或日志里有没有reload、deploy相关的记录。问测试同学报错出现之前是不是刚覆盖过 webapps 下的 war/jsp 文件。检查发布脚本确认是不是“先 kill 进程再覆盖文件”还是“覆盖完文件才重启”。结果发现测试同事用的是“覆盖式发布”进程还在跑war 包已经解压覆盖到 webapps 下面。这个操作导致 TongWeb 的类加载器已经加载了旧的 JSP 编译产物而磁盘上的 JSP 源码已经是新的二者不同步。清理 work 之后这个问题被绕过了但下次再有人不按规范发布还是会复现。3.4 第四步如果清理 work 后仍然复现查类加载器冲突清理 work 目录能解决八成问题但剩下两成不是这么简单。如果反复清理之后依旧稳定报cannot be cast to TypeBinding就要往类加载器冲突方向查。常见原因之一应用自己的 lib 目录里带了和容器冲突的包。每个 Java Web 应用都可能出现这种情况TongWeb 也不例外。比如项目的pom.xml里不小心引入了tomcat-embed-coretomcat-embed-jasperecj-*.jarEclipse JDT 编译器本体jsp-api.jar当这些 jar 出现在WEB-INF/lib里TongWeb 应用类加载器会优先加载它们。这会导致容器内部的 JSP 编译器在解析类时遇到两个不同类加载器加载的同名类类型绑定就完全错乱了。判断方法很简单用 jps 找到 TongWeb 进程后用jinfo或者看启动日志中的类路径再和应用的WEB-INF/lib做对比# 查看 TongWeb 进程的类路径确认是否包含容器的 jdt/ecj 相关 jar jinfo -l $(pgrep -f tongweb) # 或者直接检查应用 lib 里有没有冲突 jar ls /opt/tongweb/webapps/你的应用/WEB-INF/lib | grep -i -E jdt|ecj|jasper|tomcat-embed如果找到了这些包从应用依赖里排除掉即可。比如 Maven 工程在pom.xml里针对容器提供的依赖加上scopeprovided/scopedependency groupIdorg.apache.tomcat/groupId artifactIdtomcat-embed-jasper/artifactId scopeprovided/scope /dependency这步操作虽然简单但非常关键。因为我见过有团队因为这个冲突被这个异常折磨了一个下午。3.5 几个典型的排查误区排查过程中有几个常见的坑我自己也踩过顺手帮大家避一下只清了一半有人只删了某个 JSP 的对应 class 文件没删 java 源文件也没清其他残留缓存。正确做法是整个应用在 work 目录下的目录直接删掉。用重命名代替清理有人把旧 work 目录改个名字留在原地实际上编译器还是可能扫到它。直接删除最干净。混淆了work目录和temp目录temp 目录管的是上传临时文件和 tomcat 自身的临时资源JSP 编译产物只认 work。不看并发信息如果清理 work 后仍然偶发建议观察是不是高并发首次访问导致。可以让测试做一次“单用户依次访问所有 JSP 页面”的预热再上并发基本就能区分是状态错乱还是并发竞争。4. 有效的解决方案与实操步骤4.1 方案一标准清理流程90% 场景的首选如果你遇到的就是本文开头的报错且暂时不想深挖先把下面这套命令按顺序跑了# 1. 找到 TongWeb 安装目录确认实例 TOMWEB_HOME/opt/tongweb # 2. 停止服务 $TOMWEB_HOME/bin/shutdown.sh # 3. 清理 work 下对应应用的编译产物也可以直接清理所有应用 rm -rf $TOMWEB_HOME/work/Catalina/localhost/* # 4. 清理临时目录可选但推荐 rm -rf $TOMWEB_HOME/temp/* # 5. 启动 $TOMWEB_HOME/bin/startup.sh如果是 Windows 环境界面操作就手动删除work里面对应应用目录再重启。注意清理前确认操作对象是正确实例多实例部署时别把所有实例的 work 都清了避免别的实例冷启动带来额外问题。4.2 方案二调整发布方式杜绝“运行中覆盖”这个报错最常见的诱因是发布不规范。假设你每次发布都是直接把 war 解压覆盖到 webapps 下的同名应用目录TongWeb 由于在运行中类加载器不会自动卸载旧类等新的 JSP 内容触发编译时JDT 的缓存状态就乱了。所以从流程上我强烈建议发布步骤改成停止服务或者至少停掉对应的应用。删除旧的应用目录而不是覆盖。解压新的 war 包。清理 work 目录。启动服务。如果生产环境不能停机发布那就用集群滚动发布把节点从负载均衡摘掉停节点、清缓存、启节点、加回负载均衡逐个节点操作。在运行中的实例上直接覆盖 JSP 或 class 文件是这类异常的温床。4.3 方案三把 JSP 里的复杂类型逻辑挪到 Java 代码里这个方案短期不救急但长期能降低这类问题出现的概率。我见过一些老项目JSP 顶部写了一大堆%! ... %声明里面定义了复杂的泛型结构、内部类、甚至动态代理的逻辑。JSP 编译器和我们平时 IDE 里写代码不一样它要在一段混杂了 HTML 和 Java 的文本里做类型推导本身解析负担就比普通 Java 源码重。类型绑定的复杂度和出错概率是正相关的。所以建议JSP 里只放展示逻辑业务逻辑和类型处理放到 Service、Servlet 里。JSP 里的import尽量精简避免引入大量无用的类。不要在 JSP 里直接使用匿名内部类或复杂的 Java 8 类型推断代码。如果用了自定义 JSP Tag保证标签库对应的 TLD 文件干净、版本一致。这套做法不只是为了避开 TypeBinding 异常对 JSP 编译速度和页面并发性能都有正向意义。4.4 方案四升级补丁或回退版本如果你的报错在清理 work、排除冲突、规范化发布后依然稳定复现比如每次访问某个固定页面都报那就要考虑TongWeb 特定版本的已知 Bug。我在处理这类问题时会做一个“版本矩阵测试”在另一台测试机器装同版本 TongWeb。部署同一个应用复现问题。换另一个小版本比如 7049m9 或 7049m11再试。如果其他版本不复现基本可以确认是中间件 bug这时候最直接的解法是联系 TongWeb 厂商技术支持把以下几样东西准备好发过去TongWeb 完整版本号比如 7.0.4.9 M10。应用运行环境JDK 版本、操作系统版本。完整异常堆栈多抓几次说明是稳定复现还是偶发。最小化复现步骤最好能提供一个去掉业务逻辑的测试 JSP。官方一般会给补丁包替换lib下的对应 jar 即可。这类补丁通常只是替换某个 compiler 相关的类不会影响应用代码。4.5 方案五应用启动时做 JSP 预编译预热最后分享一个运维侧的小技巧。如果 JSP 首次访问并发高又不想让每个用户成为“第一个吃螃蟹的人”可以在应用启动后做一次 JSP 预编译预热。方法一用脚本请求所有入口页面强制触发编译# 应用启动后逐个访问关键 JSP 页面触发编译 for url in \ http://127.0.0.1:8080/your-app/login.jsp \ http://127.0.0.1:8080/your-app/index.jsp \ http://127.0.0.1:8080/your-app/main.jsp do curl -s -o /dev/null -w %{http_code} $url\n $url done方法二使用 JSP 预编译工具JspCTongWeb 的bin目录下一般也有直接把所有 JSP 编译成 class 放进应用目录。这样应用启动后就不会在运行期触发编译了。# 示例调用 JspC 预编译你的 JSP java -cp /opt/tongweb/lib/* org.apache.jasper.JspC -webapp /opt/tongweb/webapps/your-app -d /tmp/precompiled预编译完成后把生成的_jsp.class拷贝到应用WEB-INF/classes对应路径下JSP 请求就直接加载 class不再走运行时编译也就绕开了 JDT 并发编译的状态错乱问题。5. 同类 JDT 类型转换异常的扩展排查思路5.1 TypeBinding 之外的兄弟报错你会遇到cannot be cast to TypeBinding下次也可能遇到这些同族异常cannot be cast to com.tongweb.eclipse.jdt.internal.compiler.lookup.MethodBindingcannot be cast to com.tongweb.eclipse.jdt.internal.compiler.lookup.FieldBindingcannot be cast to com.tongweb.eclipse.jdt.internal.compiler.lookup.ReferenceBinding排查思路完全一样先看堆栈底部是不是 JSP 编译链路是的话先清 work再查类加载器冲突最后查版本 bug。这类报错本质都是 JDT 编译器在内存里维护的类型绑定表和当前编译请求不匹配导致的套路相同。5.2 定位类加载器冲突的通用方法类加载器冲突在中间件排障中非常常见除了前面说的WEB-INF/lib里带 jar还有几种隐蔽场景应用 A 和应用 B 共用一个公共类库目录而这两个应用又同时依赖不同版本的 ecj。TongWeb 的lib目录下被误放了应用 jar。JDK 版本升级后JDT 编译器和 JDK 内部类产生兼容性问题。排查时可以用jcmd或jmapdump 出 JVM 堆然后用 MATEclipse Memory Analyzer看有没有重复加载的同路径类。不过这个操作太重日常可以先从启动日志里的classload信息排查# 打印类加载过程用于观察 JDT 类是从哪个 jar 加载的 jinfo -flag TraceClassLoading $(pgrep -f tongweb)TraceClassLoading打开后类加载信息会直接输出到 stdout。看到com.tongweb.eclipse.jdt相关的类是从哪个 jar 加载的谁在捣乱一目了然。排查完记得关掉这个 flag否则日志量巨大jinfo -flag -TraceClassLoading $(pgrep -f tongweb)5.3 给运维和开发同学的几条预防建议这类问题报一次大家手忙脚乱一次。与其每次都救火不如把预防做在前面把“停机 - 删除旧应用目录 - 解压新包 - 清理 work - 启动”写进发布规范禁止运行中直接覆盖。发布完成后立即做一次 JSP 页面批量访问尽早暴露编译问题。监控 TongWeb 日志中WARNING级别的 compiler 相关异常不要等用户反馈 500 才知道。应用依赖管理严格把控容器相关的 jar 一律provided。升级 TongWeb 版本前先在测试环境验证别在生产的 M10 版本上贸然打补丁。我的经验是以上五个点只要做到前三条这个报错基本就能从你的运维生活里绝迹。最后聊点个人体会。这问题第一次出现时团队里有人怀疑是框架版本问题有人怀疑是 JDK 版本问题还有人已经开始翻业务代码了。但排到最后根子其实还是“中间件运行状态和磁盘文件不同步”这个老问题。中间件报错不要上来就把锅甩给应用代码。看到com.tongweb.eclipse.jdt这种包名的异常先记住一句口诀一清 work二查冲突三找补丁。如果你能稳定复现而且清理缓存后依旧报错那大概率是版本 bug 或类加载器冲突按文中的步骤逐项排查比你自己在业务代码里翻一个下午要高效得多。
返回列表