记一次java.lang.NoClassDefFoundError异常排查

2,769 阅读2分钟

最近在把项目发到生产环境时,遇到了这样到一个错误,解决这个问题着实花费了不少到时间:

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

在MANIFEST.MF文件中,指定引入的lib包的名字有一个时间戳,而lib文件夹下的文件名为

jae-meta-config-1.8-SNAPSHOT.jar

因此,在使用java -jar命令运行时,会找不到对应的类 随之而来的问题是,为什么会有这样的一个时间戳? 在仔细查看maven的文档后发现 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的方式启动,一切正常 至此,真相大白

总结:

  1. java -jar 和 -cp 同时使用时,-cp的配置是不生效的,java会根据MANIFEST.MF的配置来启动
  2. maven 打包时,useUniqueVersions配置如果默认时打开的,如果用到了SNATSHOP版本的依赖包,应该特别注意,要么使用java -cp的方式启动,如果要使用java -jar的方式启动,最好将该配置设为false