惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
V2EX
C
Check Point Blog
博客园_首页
B
Blog
D
Docker
U
Unit 42
量子位
I
InfoQ
有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
GbyAI
GbyAI
L
LangChain Blog
云风的 BLOG
云风的 BLOG
博客园 - Franky
美团技术团队
T
The Blog of Author Tim Ferriss
阮一峰的网络日志
阮一峰的网络日志
月光博客
月光博客
Vercel News
Vercel News
Recent Announcements
Recent Announcements
雷峰网
雷峰网
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Google DeepMind News
Google DeepMind News

冯兄话吉

当AI遇到程序员如何做加法 bug现场谜之报错也识趣-客户一上班报错就消失了! tcpdump抓包学习Nginx(反向代理),学完不怵nginx了,还总想跃跃欲试!(Nginx使用、原理完整版手册) bug现场谜之总不能告诉客户你要按F12(打开控制台)吧?(跨域详解) 程序员春节想“弯道超车”?冯兄支招另辟蹊径,或大有可为! 学习达梦数据库 bug现场谜之超级权限的root用户也存在“创建文件失败”的时候? 五年前“冯兄很笨,但他很勤奋”,五年后“好吧,冯兄很用功,但还是很平庸!”,谁为冯兄五年来被“辜负”的努力负责? bug现场谜之古老的jar包classpath玄机
bug现场谜之困在“init”方法上的那些时间!
冯兄 · 2023-07-16 · via 冯兄话吉

目录

  • 1. bug现场情况
  • 2. 尝试破案
  • 3. 真相浮出水面
  • 4. 总结
  • 更新记录

1. bug现场情况

现场将在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%不会,一定是自己哪块错了。

2. 尝试破案

回顾一下案发现场的情况,报错java.lang.NoSuchMethodError: org.eclipse.jetty.websocket.server.WebSocketServerFactory.init(Ljavax/servlet/ServletContext;)V,可是通过javap工具反编译明明有构造方法WebSocketServerFactory(javax.servlet.ServletContext)啊!

一般NoSuchMethodError异常有两种情况:

  1. classpth中该方法的类在多个jar包中,而JVM加载的jar包的那个类没有该方法。
  2. 只有一个jar包,jar包中的类没有该方法。

这两种情况归根结底是JVM运行时加载的类中确实缺失了方法。但是上面遇到的问题查找加载类是存在报错的构造方法的。

如果JVMclasspth中有多个包存在同一个class,到底JVM会加载哪个包中的class是平台相关的(Linux系统和Windows系统上可能加载的不是同一个jar包)。需要注意:JVMclasspth下的jar包中load对应的class文件,这跟jar包的命名没有关系。

可以通过以下方法根据报错信息定位加载的jar包:

1). JVM使用参数-verbose:class,这个参数能够输出加载classjar包绝对路径。

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,百思不得其解!

3. 真相浮出水面

怎么办呢?问题总是要解决的。

在开发环境上准备调试代码,突然意识到报错中的init是不是一个普通方法啊?

赶快看看反编译的代码发现确实没有init普通方法,只有init构造方法。问题就出在这里,查了一下发现jetty9.3升级到9.4的时候对WebSocketServerFactoryinit普通方法改为构造方法

这是笔者的一个知识误区,以为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

对于一个程序员来说,异常的的堆栈信息是司空见惯的,也就懒得深究其中的一些玄机,果然“报应不爽”!出来混,迟早要还的。

4. 总结

  1. 很多看似玄学的bug解释不了,最后原因总是归结为“知识的盲区”。很多知识不必懂的很深入,但是基本的东西要了解,此时“不求甚解”,彼时“这是玄学?”。
  2. 有些时候会无意识的想当然一些结论(比如本示例中.init()方法自然认为是构造方法)。没办法十分敲定的东西,要多查一查,多一份思路。
  3. 排查问题,针对一个思路要充满信心,即使这个思路不能解决问题,至少也要能得出这条思路的结论。不能急躁、粗心、盲目尝试。思路窄了,就停下来,明天再尝试,避免进入死胡同。

更新记录

  • 2022-01-24 16:10 微信公众号“冯兄画戟”文章发表前重读、优化、勘误
  • 2022-01-26 15:20 掘金专栏发表前重读、优化、勘误

  • 文章底部评论区欢迎留言
  • 微信公众号“冯兄画戟”留言:文章链接
  • 邮箱私信:fengmengzhao@gmail.com