当你看到
NoSuchMethodError
的时候,不要慌,深呼吸,这可能只是JAR包版本的问题…
那是一个看似平常的周二下午,系统运行良好,开发团队在有条不紊地推进着新功能的开发。突然,测试环境中的报表导出功能失效了,用户反馈页面卡住,后台日志疯狂刷屏:
java.lang.NoSuchMethodError: 'byte[] org.apache.poi.util.IOUtils.peekFirstNBytes(java.io.InputStream, int)'
作为职业填坑人,我看到这个错误的第一反应是:“这是依赖不对?”
先来仔细分析一下错误信息:
Caused by: java.lang.NoSuchMethodError: 'byte[] org.apache.poi.util.IOUtils.peekFirstNBytes(java.io.InputStream, int)'
at org.apache.poi.poifs.filesystem.FileMagic.valueOf(FileMagic.java:209)
at org.apache.poi.openxml4j.opc.internal.ZipHelper.verifyZipHeader(ZipHelper.java:143)
at org.apache.poi.openxml4j.opc.internal.ZipHelper.openZipStream(ZipHelper.java:175)
at org.apache.poi.openxml4j.opc.ZipPackage.(ZipPackage.java:130)
at org.apache.poi.openxml4j.opc.OPCPackage.open(OPCPackage.java:319)
at ca.terrasoft.poi.POIUtil.openFile(POIUtil.java:57)
从堆栈可以看出:
IOUtils.peekFirstNBytes
方法openFile
和ZipPackage
)这应该是一个典型的JAR包版本不一致问题。简单说,代码期望调用的方法在运行时环境中找不到,通常是因为编译时使用的库版本与运行时加载的版本不同。
由于项目是一个20年前的古董JSP项目,没有使用Maven、Gradle等现代构建工具,所有依赖都直接堆在WEB-INF/lib
目录下。我们只能通过手动和脚本方式排查依赖。
$ ls -la /webapps/myapp/WEB-INF/lib/poi-*.jar
.....
-rw-r--r-- 1 tomcat tomcat 2758112 May 10 14:32 poi-5.2.3.jar
表面上看,POI的版本是5.2.3,应该没问题,会不会是某个古董jar中可能有依赖老版本poi,那咋整? 一个一个去翻所有jar包? 嗯嗯,这好像不是码农该做的事。
为了更详细地了解web容器中类加载情况,直接整一个简单的诊断页面:
<%@ page import="java.net.URL" %>
<%@ page import="java.util.Enumeration" %>
<%@ page import="java.io.File" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
Classpath Info
Classpath Information
Finding POI libraries:
<%
// 查找指定类的位置
try {
Class> clazz = Class.forName("org.apache.poi.util.IOUtils");
out.println("- IOUtils class found at: " + clazz.getProtectionDomain().getCodeSource().getLocation() + "
");
// 查找方法是否存在
try {
clazz.getDeclaredMethod("peekFirstNBytes", java.io.InputStream.class, int.class);
out.println("- peekFirstNBytes method exists!
");
} catch (NoSuchMethodException e) {
out.println("- peekFirstNBytes method NOT found!
");
}
} catch (Exception e) {
out.println("- Error: " + e.getMessage() + "
");
}
%>
All POI related JARs:
<%
ClassLoader cl = Thread.currentThread().getContextClassLoader();
try {
Enumeration resources = cl.getResources("META-INF/MANIFEST.MF");
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
String path = url.getPath();
if (path.contains("poi")) {
out.println("- " + path + "
");
}
}
} catch (Exception e) {
out.println("- Error: " + e.getMessage() + "
");
}
%>
运行后,页面输出:
IOUtils class found at: file:/Users/xdev/workdir/myProject/classes/artifacts/myProject_war_exploded/WEB-INF/lib/tm-extractors.jar
* peekFirstNBytes method NOT found! .
这说明,虽然我们有poi-5.2.3.jar,但实际被加载的IOUtils
类却来自tm-extractors.jar
!
进一步到Maven仓库查询tm-extractors.jar
(https://mvnrepository.com/artifact/org.textmining/tm-extractors/0.4),发现它自带了poi-2.5.1.jar
,而且tm-extractors
的发布时间非常久远。
也就是说,tm-extractors.jar中自带的老版本POI类覆盖了新版本POI的类加载,导致我们即使有poi-5.2.3.jar,实际运行时却用的是2.5.1的实现,自然没有peekFirstNBytes
方法。
最终我们发现,问题出在tm-extractors.jar
这个古老依赖。它内部包含了POI 2.5.1的class文件,且优先被类加载器加载,覆盖了我们显式依赖的poi-5.2.3。
具体来说:
WEB-INF/lib
目录下还存在tm-extractors.jar
,它内部自带poi 2.5.1IOUtils
等POI类被加载自tm-extractors.jar
这就是经典的依赖地狱(Dependency Hell),而且在没有构建工具的老项目中更为棘手。
对于没有构建工具的老JSP项目,解决这类问题通常只能靠手动:
WEB-INF/lib
目录,移除tm-extractors.jar
或用工具(如jar命令)剥离其中的POI相关class文件tm-extractors
功能,尝试寻找不自带POI的版本,或用更现代的替代库tm-extractors.jar
考虑到项目情况,我们最终选择了方案一:手动清理WEB-INF/lib
目录,移除tm-extractors.jar
,只保留poi-5.2.3.jar。这样虽然失去了一些老库的功能,但保证了POI相关功能的正常运行。
具体步骤:
jar tf
命令查看)痛定思痛,我们制定了一系列措施来防止类似问题再次发生:
这次POI依赖踩坑的经历,让我们深刻认识到了Java生态系统中依赖管理的重要性。对于没有构建工具的老项目,依赖冲突更容易发生且更难排查。
关键在于:
NoSuchMethodError
这类错误时最后,我想用一句话来结束这篇文章:
在Java世界中,了解你的依赖就像了解你的朋友一样重要,当它们和平相处时,你的应用才能健康成长。
希望我的经验能帮助到同样面临依赖问题的开发者们。记住,你不是一个人在战斗!