现场从Docker
上迁移一个应用到Linux主机。
使用命令docker exec -it $CONTAINER /bin/sh
进入容器,ps -ef
发现只有一个进程:jar -jar xxx.jar
。将jar包解压缩,如图所示:
和springboot的jar包结构不一样,这里面直接是class、配置文件及META-INF
目录。
看了一下env
环境变量,也没有CLASSPATH
值。心里想着奇怪(那些依赖jar包是从哪里加载的呢?),但是也没有想明白咋回事,暂且不管。
把jar包迁移到Linux服务器上,尝试用java -jar xxx.jar
启动,果然报错一大堆基础的类找不到。这时候突然想起在原docker容器jar包同目录中有一个SYNC_lib
目录,该目录似乎包含了jar包依赖的第三方包。
将SYNC_lib
目录也迁移到jar包同级目录上,指定classpath
重新启动:java -cp .:SYNC_lib/*: xxx.jar
。这时候相关的类都加载了,有一个报错是数据库的驱动不是最新的。将原驱动备份,复制一个新的驱动到SYNC_lib
目录内:
mv SYNC_lib/Postgresql-old-version.jar SYNC_lib/Postgresql-old-version.jar.bak
cp /path/Postgresql-new-version.jar SYNC_lib/
自信满满地使用java -cp .:SYNC_lib/*: xxx.jar
重新启动,报错:ClassNotFoundException: org.postgresql.Driver not found
。
什么情况?明明新的驱动包已经在classpath
里面了,为什么会找不到class呢?笔者还特意将新的驱动包解压缩确认是能够找到org.postgresql.Driver
类的。
更奇怪的是切换到旧版本的驱动包就能够加载org.postgresql.Driver
驱动类了(通过JVM参数-verbose
能在日志中打印出加载的详细类和对应的jar包)。
Docker学习参考https://fengmengzhao.github.io/2021/06/25/docker-handbook-2021.html。
总结一下案发现场疑点:
java -jar xxx.jar
启动,命令行和CLASSPATH
环境变量都没有指定SYNC_lib
路径,该JVM实例是怎么加载这些第三方jar
包的?classpath
启动jar包,只是替换了classpath
下jar包的版本,竟然报错ClassNotFoundException
?没思路了,只能手动写个代码看看从指定的classpath
下能不能加载对应的class:
import java.security.*;
public class FindClass {
public static void main(String args[]) {
Class<?> clazz = null;
try {
clazz = Class.forName("com.uxun.uxunplat.util.OperateResult");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
CodeSource cs = clazz.getProtectionDomain().getCodeSource();
String location = cs.getLocation().getPath();
System.out.println(location);
}
}
结果:能成功加载org.postgresql.Driver
,说明新的驱动jar包是没问题的,命令行参数-classpath
(或者-cp
)的设定方法也是正确的。
百思不得其解……
突然,灵机一动,可以不使用jar包启动,而是直接启动包含main()
方法的class类,说不定是jar包在作妖。
main()
方法启动:
#这里在-cp中增加xxx.jar
#也就是将之前-jar启动的应用加入到classpath中
java -cp .:SYNC_lib/*:xxx.jar: com.xxx.xxx
应用成功启动了!
果不其然,是jar包在作妖。此时笔者再次打开jar
,这次没有忽略任何细节,打开META-INF\MANIFEST.MF
文件,如图:
原来玄机在这里,捶胸顿足,悔之晚矣!
META-INF\MANIFEST.MF
文件是jar包的元数据文件,该文件指明了:
Main-Class
:该jar包的入口类(包含main
方法的类)。Class-Path
:依赖jar包的classpath路径。jar包路径之间使用空格分隔。破案:
classpath
也能够加载第三方依赖,不是玄学,而是classpath
在jar包内指定了。ClassNotFoundException
,是因为在MANIFEST.MF
文件中定义的classpath
会覆盖掉命令行中指定的-classpath
参数设置。也就是说命令行中正确指定的-classpath
实际上并没有生效(不是参数设定错误,而是参数被覆盖了)。实际上,回到”刀耕火种”的时代,在没有构建工具(例如ant
、maven
等)时,构建一个有第三方依赖的java程序,可以使用命令:
#参数c表示创建jar归档文件
#参数v表示打印详细日志
#参数f表示指定jar的名称,这里是xxx.jar
#参数m表示指定元数据信息文件,这里是MANIFEST.txt
jar cvfm xxx.jar MANIFEST.txt com.xxx.xxx
关于jar
打包和MANIFEST.MF
更多内容参考:https://docs.oracle.com/javase/tutorial/deployment/jar/downman.html。
现代开发java程序用IDE集成开发环境,不需要手动敲命令。例如,用IDEA导出一个jar包:
1). 在项目中增加一个Artifacts
。
2). 执行构建,导出jar文件。
即使现代程序开发用IDE方便了很多,基础知识(如jar包中
MANIFEST
到底是什么?有什么作用?)的掌握有利于对编程体系的理解。
java -jar xxx.jar
能够正常运行;也没有打开jar包时顺便看看MANIFEST.MF
文件内容。如果这两个任意一个做了,在前30%时间内就能破案。ClassNotFoundException
,要确信不是高版本驱动不可用,而是依赖jar包加载有问题,这时候千万不能跑偏。ClassNotFoundException
,实际上可以认定命令行参数没有最终起作用(本例被jar包内MANIFEST
文件覆盖了)。当然了,认知可能会有盲区,多一步验证,如果发现认知盲区,要搞明白关联知识。