现场将在Tomcat 8.5
中运行的war
包迁移到jetty 9.4.19
上,启动容器后报错:
org.springframework.context.ApplicationContextException: Failed to start bean 'stompWebSocketHandlerMapping'; nested exception is java.lang.NoSuchMethodError: org.eclipse.jetty.websocket.server.WebSocketServerFactory.init(Ljavax/servlet/ServletContext;)V
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:176) ~[spring-context-4.2.3.RELEASE.jar:4.2.3.RELEASE]
...
...
NoSuchMethodError
应该是看到后最有头绪一个错误了:“在加载到JVM
的对应类中找不到当前调用的方法”。
如果编译环境中对应类没有对应的方法,是不能编译成功的(集成开发环境会报错)。如果编译成功后部署时候报错NoSuchMethodError
,说明运行时和编译时依赖的类不一致。
这里说的“编译时依赖”指的是:构建工具在编译时
CLASSPATH
中依赖的class
;“运行时依赖”指的是:JVM
实例运行时加载到JVM
中的class
。对于同一个class loader
,只会成功加载一次class
。
上面的异常堆栈显示:类org.eclipse.jetty.websocket.server.WebSocketServerFactory没有构造方法WebSocketServerFactory(javax.servlet.ServletContext)。那就看一看运行时依赖的类有没有对应的构造方法吧。
现场的情况下,只能用javap
命令,但是首先你要找到这个类是从哪个jar
包加载的,如何根据类找到加载的jar
包路径在接下来的尝试破案做进一步说明。
javap -cp lib/websocket/websocket-server-9.4.41.v20210516.jar org.eclipse.jetty.websocket.server.WebSocketServerFactory
注意这里如果使用
javap -cp lib/websocket/* xxxx
这样指定classpath
用*
配置的方式无效,但是对于javac
命令是有效的。
这不是存在WebSocketServerFactory(ServletContext context)
构造方法吗???
后来笔者又尝试了多种途径确认这个构造方法是存在的,但是却报错NoSuchMethodError
,网上一大堆找“java.lang.nosuchmethoderror but method exists”,无果。因为网上说的最后都证明确实没有对应的方法。
但本案发现场的情况是它有啊!现场变得诡异起来了!难道笔者找到了一个超级bug
?直觉告诉我100%不会,一定是自己哪块错了。
回顾一下案发现场的情况,报错java.lang.NoSuchMethodError: org.eclipse.jetty.websocket.server.WebSocketServerFactory.init(Ljavax/servlet/ServletContext;)V
,可是通过javap
工具反编译明明有构造方法WebSocketServerFactory(javax.servlet.ServletContext)
啊!
一般NoSuchMethodError
异常有两种情况:
classpth
中该方法的类在多个jar包中,而JVM加载的jar包的那个类没有该方法。jar
包,jar
包中的类没有该方法。这两种情况归根结底是JVM运行时加载的类中确实缺失了方法。但是上面遇到的问题查找加载类是存在报错的构造方法的。
如果
JVM
的classpth
中有多个包存在同一个class
,到底JVM
会加载哪个包中的class
是平台相关的(Linux
系统和Windows
系统上可能加载的不是同一个jar
包)。需要注意:JVM
从classpth
下的jar
包中load
对应的class
文件,这跟jar
包的命名没有关系。
可以通过以下方法根据报错信息定位加载的jar包:
1). JVM
使用参数-verbose:class
,这个参数能够输出加载class
的jar
包绝对路径。
2). 使用java代码:
Class<?> clazz = null;
try {
clazz = Class.forName("org.eclipse.jetty.websocket.server.WebSocketServerFactory");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
CodeSource cs = clazz.getProtectionDomain().getCodeSource();
String location = cs.getLocation().getPath();
System.out.println(location);
3). 使用linux命令:
for file in *.jar; do
echo $file;
jar tvf $file |grep WebSocketServerFactory
done
在允许重启系统或者启动的JVM
中设置了--verbose:class
参数的话“1)”方法是最方便的,可以直接在日志中查找对用的类。
不允许重启JVM
的话可以采用方法“2)”,但是要指定正确的classpath
,否则加载不到对应的类。查找classpath
可以从jvm
对应的进程中查找。
对于
springboot
框架打成的jar
包,一般依赖都打进在jar
包中了;对于severlet
容器使用的war
包,依赖除了WEB-INF/lib
外还包括容器安装目录下的lib
包;对于普通的jar
包,依赖可能定义在了MANIFEST
元文件中(更多关于MANIFEST
内容可以参考:https://fengmengzhao.github.io/2021/12/18/bug-scene-of-old-jar-classpath-mystery.html。
如果想查找指定目录的哪个jar
包含有某个class
,可以使用“3)”方法,列出jar
中包含的文件清单并查找匹配。
为什么要费劲找到报错类是从哪个jar
包中加载的呢?一来jar
包一般能提供版本相关的信息;二来javap
命令是需要指定jar
包作为classpath
才能成功反编译。
使用javap
命令反编译,语法如下:
#这种方式是指定类信息和类所在的jar包为classpath反编译
javap [-verbose] -cp /some/path/to/lib/xxx.jar com.xx.SomeClass
#这种方式是将class文件从jar中解压,直接反编译class文件
mkdir dir
cd dir
jar xvf ../SomeClass-belong-to.jar
javap [-verbose] com/xx/SomeClass.class
javap
的-verbose
参数展示class
文件的详细编译信息,如果只想判断是是否有某个方法,可以不加-verbose
参数。
通过上面的方法确认本示例的情况:明明方法存在啊,为什么NoSuchMethodError
,百思不得其解!
怎么办呢?问题总是要解决的。
在开发环境上准备调试代码,突然意识到报错中的init
是不是一个普通方法啊?
赶快看看反编译的代码发现确实没有init
普通方法,只有init
构造方法。问题就出在这里,查了一下发现jetty
在9.3
升级到9.4
的时候对WebSocketServerFactory
的init
由普通方法改为构造方法。
这是笔者的一个知识误区,以为WebSocketServerFactory.init(Ljavax/servlet/ServletContext;)V
是一个构造方法,实际上如果是构造方法报错是长这样的:
10:24:09.590 [main] ERROR org.springframework.boot.SpringApplication - Application run failed
java.lang.NoSuchMethodError: org.springframework.boot.builder.SpringApplicationBuilder.<init>([Ljava/lang/Object;)V
普通方法和构造方法实际上就是.init()
和.<init>()
的区别。
这里报错中
.<init>([Ljava/lang/Object;)V
中.
表示是一个方法的调用;<init>
表示构造方法的调用;[
表示一个数组;Ljava/lang/Object;
表示java.lang.object
对象;V
表示返回类型是void
。实际上就是SpringApplicationBuilder(java.lang.Object...)
的构造方法,方法的参数是java.lang.Object
数组。这种写法和class
文件的内部表示是一致的。jvm
更多内部实现 类型表示参考:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
对于一个程序员来说,异常的的堆栈信息是司空见惯的,也就懒得深究其中的一些玄机,果然“报应不爽”!出来混,迟早要还的。
bug
解释不了,最后原因总是归结为“知识的盲区”。很多知识不必懂的很深入,但是基本的东西要了解,此时“不求甚解”,彼时“这是玄学?”。.init()
方法自然认为是构造方法)。没办法十分敲定的东西,要多查一查,多一份思路。