记一次java.lang.ClassNotFoundException问题排查过程
同事提供一个or-simulation-engine.jar包(非maven项目,内部依赖很多其他jar,这个包是手动打出来的)给我,我集成到我的springboot项目中,在本地IDEA启动Springboot后,相关功能都是正常的;但是将Springboot项目打成app.jar后,使用java -jar app.jar方式启动后,运行时爆出java.lang.ClassNotFoundException: com.anylogic.libraries.modules.markup_descriptors.DescriptorFactory
。
为什么IDEA可以执行,打成jar包使用java -jar就执行不了呢?
以下内容都是使用java -jar app.jar
测试的结果。
一、代码定位
jar包依赖关系:我的app.jar依赖第二方or-simulation-engine.jar,而or-simulation-engine.jar依赖第三方com.anylogic.engine.jar
通过分析代码得知,or-simulation-engine.jar包在运行时,调用了如下代码:
String name = "com.anylogic.libraries.modules.markup_descriptors.DescriptorFactory";
ClassLoader systemCL = ClassLoader.getSystemClassLoader();
Class clazz = systemCL.loadClass(name);
这个代码是anylogic的jar包:com.anylogic.engine.jar中的内容。
具体异常信息:
java.lang.ClassNotFoundException: com.anylogic.libraries.modules.markup_descriptors.DescriptorFactory
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at com.yonghui.or.simulation.controller.CoreController.test(CoreController.java:99)
at com.yonghui.or.simulation.controller.CoreController$$FastClassBySpringCGLIB$$6a496143.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:749)
通过异常信息可以看出:ClassLoader.getSystemClassLoader()获得的是AppClassLoader,但是AppClassLoader并没有在对应的路径下加载到该类。但是该类确实是存在的,而且通过new或者Class.forname()都是可以找到该类的。
可以看到,or-simulation-engine.jar手动打完包后,内部依赖的jar都被放到了一起,不是以jar包的方式存在的。
这可能是非maven项目or-simulation-engine手动打包有问题,导致集成到springboot项目打成app.jar后找不到该类了。
二、确定使用的ClassLoader
那么异常中的这个类com.anylogic.libraries.modules.markup_descriptors.DescriptorFactory
应该使用哪个classloader加载呢?
可以使用jvm调优工具arthas,找到app.jar进程后,输入 sc -d com.anylogic.libraries.modules.markup_descriptors.DescriptorFactory
命令,查看该类使用的类加载的情况:
可以看到这里使用的是LaunchedURLClassLoader。而该加载器的上级才是AppClassLoader。
最终运行的java -jar app.jar
的jar包是通过spring-boot-maven-plugin这个插件生成的, JAR中依赖的各个jar文件其实并不在运行时应用的classpath下(实际在app.jar/BOOT-INF/lib下存放所有依赖的jar包),也就是根据类加载的双亲委派机制,这些依赖没办法被默认的任何一个classloader加载,Springboot为了解决这个问题,自定义了类加载机制,LaunchedURLClassLoader就是Springboot自定义的类加载器。关于Springboot的类加载器可以查看:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<scope>Provided</scope>
</dependency>
查看app.jar/META-INFO/MANIFEST.MF的内容,可以看到使用了spring-boot-loader包中的org.springframework.boot.loader.JarLauncher。这个类最后创建的就是LaunchedURLClassLoader。
三、问题确认
通过上面的分析,可以确认问题的原因: app.jar依赖第二方or-simulation-engine.jar,而or-simulation-engine.jar依赖第三方com.anylogic.engine.jar。第三方的jar包为了代码安全,给代码做了相关的混淆等操作后,在代码运行时,使用动态ClassLoader.getSystemClassLoader()类加载器(AppClassLoader)动态加载自己的一个类;当所有的代码集成到springboot项目并用springboot-maven插件打包后,ClassLoader.getSystemClassLoader()就找不到对应的类了。
IDEA中启动不会有问题,是因为IDEA默认使用的是ApplicationClassLoader进行类加载的,而且classpath对应了多个jar包,包括jre/lib 、jre/lib/ext、本地mvn仓库、和app.jar项目路径。而java -jar方式的classpath只有app.jar。
四、解决方法
找到问题后,就可以针对性解决问题了。有两种方式:
1.改变打包方式,让打包后的代码内被ClassLoader.getSystemClassLoader()这个AppClassLoader找到;
2.修改第三方com.anylogic.engine.jar,将类加载器改成LaunchedURLClassLoader。
这两种方式的目的都是class文件放到所使用的类加载器对应的路径下。
方式一
将打包方式改成:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.example.helloloader.HelloLoaderApplication</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.directory}/lib
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
方式二
修改第三方com.anylogic.engine.jar,将类加载器改成LaunchedURLClassLoader。其实就是修改一行代码:
将:
ClassLoader systemCL = ClassLoader.getSystemClassLoader();
修改成:
ClassLoader systemCL = Thread.currentThread().getContextClassLoader();
这样app.jar在运行时就可以获得LaunchedURLClassLoader。
这种方式需要反编译,如果将第三方com.anylogic.engine.jar整体反编译,部分class会编译失败,修改代码后也很难再编译成功。
这里有个简单的方式:新建一个空maven项目,在pom中引入本地的com.anylogic.engine.jar。然后按照要修改的class文件在第三方jar包内的包名,在该空项目中建相同的包和类名(com.anylogic.engine.markup.descriptors.IDescriptorFactory),并将反编译后的内容放入这个类中,再修改掉对应的一行代码。通过mvn clean packge
重新打包,在target目录下找到这个IDescriptorFactory.class文件。
然后用这个IDescriptorFactory.class替换掉第三方com.anylogic.engine.jar所对应的IDescriptorFactory.class.
如何替换?
首先解压:
jar -xvf com.anylogic.engine.jar
然后找到并替换掉IDescriptorFactory.class
最后成jar包
jar cvfM com.anylogic.engine.jar ./
将新的jar包集成到项目中后就可以启动了。
参考:
https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html#appendix.executable-jar.restrictions
https://blog.csdn.net/wuxiaolongah/article/details/129245218
https://blog.csdn.net/kingwinstar/article/details/125482503
https://www.jianshu.com/p/1ec1189f2397