现场将在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()方法自然认为是构造方法)。没办法十分敲定的东西,要多查一查,多一份思路。