最近在把项目发到生产环境时,遇到了这样到一个错误,解决这个问题着实花费了不少到时间:
Exception in thread "main" java.lang.NoClassDefFoundError: com/xxx/xxx/MetaConfigClientImpl
at com.xxx.xxx.notify.cp.utils.PropertyHolder.<clinit>(PropertyHolder.java:25)
at com.xxx.xxx.notify.cp.realtime.RealTimeMarketNotifyMain.run(RealTimeMarketNotifyMain.java:43)
at com.xxx.xxx.notify.cp.realtime.RealTimeMarketNotifyMain.main(RealTimeMarketNotifyMain.java:29)
Caused by: java.lang.ClassNotFoundException: com.xxx.xxx.MetaConfigClientImpl
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
... 3 more
MetaConfigClientImpl 这个类是我自己写到一个工具类,通过maven依赖到方式引入到项目中,依赖方式如下:
<dependency>
<groupId>com.xxx.xxx</groupId>
<artifactId>jae-meta-config</artifactId>
<version>1.8-SNAPSHOT</version>
</dependency>
该包在公司的很多项目都有使用到,从来没有出现这种问题,根据自己到经验,一般这种开发没问题,生产环境找不到包到情况,都是依赖冲突导致的。因此首先把重点放在依赖冲突的排查上面,先通过maven dependency tree来看,没发现与该包的冲突,反而是其他一些日志依赖之类的冲突去掉了不少,后来通过idea的插件dependency analyzer来排查,还是没发现有冲突的地方。本着试一试的心态,将一些不相关的冲突去掉后,重新发到生产环境上,结果不出意外,还是不行。 既然不是依赖冲突的问题,由于本人的开发环境是mac,生产环境是linux,按理来说区别不大,因此生产跟开发环境的差别就剩打包跟运行这两个地方,该项目的打包命令如下:
<build>
<sourceDirectory>src/main/java</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.5</version>
<executions>
<execution>
<id>copy-config</id>
<phase>process-sources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/conf</outputDirectory>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<excludes>
<exclude>*</exclude>
</excludes>
<outputDirectory>${project.build.directory}/jar</outputDirectory>
<archive>
<manifest>
<mainClass>com.xxx.xxx.xxx.xxx.xxxx.RealTimeMarketNotifyMain</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
<manifestEntries>
<Class-Path>conf/</Class-Path>
</manifestEntries>
</archive>
<classesDirectory/>
</configuration>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib/</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
打包后,会将配置文件放在conf目录下,依赖包放在lib目录下。生产环境的lib目录下是能看到jae-meta-config这个依赖的,将lib目录下所有的文件进行md5,而后将开发环境下lib包目录的所有文件进行md5,两者进行string diff对比,发现一模一样。打出来的依赖包明明都一样,这说明应该不是依赖包的问题。 而后看运行的方式,打开生产环境的启动脚本,发现启动命令如下:
exec gosu ubuntu java -Xmx${memory}m -Xms${memory}m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xloggc:/app/logs/gc-%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50000K -XX:+ExitOnOutOfMemoryError -cp \"conf/:jar/*:lib/*\" -jar /app/RealTimeMarketNotifyMain.jar com.xxx.xxx.xxx.xxx.xxx.RealTimeMarketNotifyMain
而我在本地启动时是直接用 java -cp "conf/:jar/:lib/" com.xxx.xxx.xxx.xxx.xxx.RealTimeMarketNotifyMain com.xxx.xxx.xxx.xxx.xxx.RealTimeMarketNotifyMain启动的,查阅资料后发现,使用java -jar 时,-cp是不生效的,而是会用到META-INF\MANIFEST.MF 文件的配置启动,尝试了一下,在生产环境使用java -cp命令启动发现是能够正常启动的。至此,问题可以定位到启动方式之间的差距导致的。 但是从上面的打包命令上可以看到,我们在maven里面指定了主类,依赖包路径已经配置文件的路径,按理来说直接用java -jar的方式启动也是没问题的
<archive>
<manifest>
<mainClass>com.xxx.xxx.xxx.xxx.xxxx.RealTimeMarketNotifyMain</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
<manifestEntries>
<Class-Path>conf/</Class-Path>
</manifestEntries>
</archive>
使用jar-gui解开jar包,查看里面的MANIFEST.MF文件,终于发现问题:
在MANIFEST.MF文件中,指定引入的lib包的名字有一个时间戳,而lib文件夹下的文件名为
jae-meta-config-1.8-SNAPSHOT.jar
因此,在使用java -jar命令运行时,会找不到对应的类
随之而来的问题是,为什么会有这样的一个时间戳?
在仔细查看maven的文档后发现
maven有一个useUniqueVersions的配置,该配置的作用是在生产MANIFES.MF文件时,SNATSHOP版本的jar包是否要打上唯一版本时间戳,该配置默认值为true,因此jae-meta-config-1.8-SNAPSHOT.jar 变成了 jae-jbaseclient-2.0.2-20191104.060124-1.jar。导致找不到对应的jar包,启动报错。重新打包配置,将该配置设为false
<archive>
<manifest>
<mainClass>com.xxx.xxx.xxx.xxx.xxx.RealTimeMarketNotifyMain</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<useUniqueVersions>false</useUniqueVersions>
</manifest>
<manifestEntries>
<Class-Path>conf/</Class-Path>
</manifestEntries>
</archive>
而后使用java -jar的方式启动,一切正常 至此,真相大白
总结:
- java -jar 和 -cp 同时使用时,-cp的配置是不生效的,java会根据MANIFEST.MF的配置来启动
- maven 打包时,useUniqueVersions配置如果默认时打开的,如果用到了SNATSHOP版本的依赖包,应该特别注意,要么使用java -cp的方式启动,如果要使用java -jar的方式启动,最好将该配置设为false