现场从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文件覆盖了)。当然了,认知可能会有盲区,多一步验证,如果发现认知盲区,要搞明白关联知识。