赞
踩
同事提供一个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形式测试。
jar包依赖关系:我的app.jar依赖第二方or-simulation-engine.jar,而or-simulation-engine.jar依赖第三方com.anylogic.engine.jar
通过分析代码得知,app.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后找不到该类了。
那么异常中的这个类com.anylogic.libraries.modules.markup_descriptors.DescriptorFactory
应该使用哪个classloader加载呢?
可以使用jvm调优工具arthas,找到app.jar进程后,输入sc -d com.anylogic.libraries.modules.markup_descriptors.DescriptorFactory
命令,查看该类使用的类加载的情况:
可以看到这里使用的是LaunchedURLClassLoader。而该加载器的上级才是AppClassLoader。
app.jar
是通过spring-boot-maven-plugin这个插件生成的, app.jar中依赖的各个jar文件其实并不在运行时应用的classpath下(实际在app.jar/BOOT-INF/lib下存放所有依赖的jar包),也就是根据类加载的双亲委派机制,这些依赖没办法被默认的任何一个classloader加载,Springboot为了解决这个问题,自定义了类加载机制,LaunchedURLClassLoader就是Springboot自定义的类加载器。
app.jar解压后可以看到内部结构:
其中,app.jar/BOOT-INF/classes下放我们自己写的代码编译的class;app.jar/BOOT-INF/lib - 存放所有依赖的jar包;
查看app.jar/META-INFO/MANIFEST.MF的内容,这个文件有app.jar这个包加载过程需要的相关信息,包括Main-class、Start-calss、Spring-Boot-Classes、Spring-Boot-Lib等相关信息。可以看到主类class是org.springframework.boot.loader.JarLauncher。这个主类是spring-boot-loader包中,这个类最后创建的就是LaunchedURLClassLoader。
关于Springboot的类加载可以查看spring-boot-loader包:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-loader</artifactId> <scope>Provided</scope> </dependency>
找到spring-boot-loader包中的org.springframework.boot.loader.JarLauncher。可以看到有两个静态变量BOOT_INF_CLASSES和BOOT_INF_LIB,表示class和库的路径;这里有个main方法入口:
打开launch方法:
可以看到最终创建的就是LaunchedURLClassLoader。
通过launch方法把LaunchedURLClassLoader加入到当前线程的上下文中Thread.currentThread().setContextClassLoader(classLoader)
:
Thread.currentThread().setContextClassLoader(loader)
方法用于设置当前线程的上下文类加载器,即把给定的ClassLoader设置为当前线程的上下文类加载器。上下文类加载器是一个ClassLoader,它是由线程的创建者设置的,并且在线程中保持不变。如果没有显式地设置上下文类加载器,那么线程将继承其创建者的上下文类加载器。
LaunchedURLClassLoader的作用是加载app.jar这样的目录结构中的class,能够找到要加载的class(比如BOOT-INF目录中的class或jar),并且加载他们。
LaunchedURLClassLoader继承了java.net.URLClassLoader,自己实现了loadClass方法,并且在自己的loadClass方法中还是调了super.loadClass,也就是java.lang.ClassLoader#loadClass,这其实又回到了双亲委派机制:
LaunchedURLClassLoader是怎么加载我们在springboot应用中定义的mainclass,也就是app.jar应用的入口程序呢? 在org.springframework.boot.loader.MainMethodRunner中,通过LaunchedURLClassLoader load并且通过反射调用了main 方法。
其中mainClassName
就是从MANIFEST.MF中找到的Start-Class对应的类名:
Java -Jar是用LaunchedURLClassLoader来加载class和依赖jar包的。而在IDE中则是直接以ApplicationClassLoader来加载的。
通过IDEA和java -jar运行分别运行下面代码,可以看到application加载路径
是不一样的,IDEA的路径包括jre/lib、jre/lib/ext、本地maven仓库、本地项目路径等,这些路径正好能被IDEA的默认类加载器ApplicationClassLoader加载到。
而使用LaunchedURLClassLoader加载器的java -jar app.jar方式启动,是能够加载读取app.jar/BOOT-INF/classes和app.jar/BOOT-INF/lib路径的class的;但是app.jar内部的class如果再自定义使用ApplicationClassLoader加载app.jar/BOOT-INF内的class就会出现找不到class的情况;这时ApplicationClassLoader的application加载路径
只有app.jar。
System.out.println("不同类加载器加载的路径"); System.out.println("bootstap加载路径: " + System.getProperty("sun.boot.class.path")); System.out.println("extantion加载路径: " + System.getProperty("java.ext.dirs")); System.out.println("application加载路径: " + System.getProperty("java.class.path"));
通过上面的分析,可以确认问题的原因: app.jar依赖第二方or-simulation-engine.jar,而or-simulation-engine.jar依赖第三方com.anylogic.engine.jar。第三方的jar包为了代码安全,给代码做了相关的混淆等操作后,在代码运行时,使用动态加载ClassLoader.getSystemClassLoader()动态加载自己的一个类;当所有的代码集成到springboot项目并用springboot-maven插件打包后,ClassLoader.getSystemClassLoader()就找不到对应的类了。
找到问题后,就可以针对性解决问题了。有两种方式:
1.改变打包方式,让打包后的代码能被ClassLoader.getSystemClassLoader()这个classLoader找到;
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();
Thread.currentThread().getContextClassLoader()
的意义是:父Classloader可以使用当前线程上下文中指定的classloader中加载的类。颠覆了父ClassLoader不能使用子Classloader或者是其它没有直接父子关系的Classloader中加载的类这种情况。它是由线程的创建者设置的,并且在线程中保持不变。如果没有显式地设置上下文类加载器,那么线程将继承其创建者的上下文类加载器。
这样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包集成到项目中后就可以启动了。
参考:
使用spring-boot-maven-plugin插件将本地jar包打入包中_诸葛小猿的博客-CSDN博客
记一次springboot项目结合arthas排查ClassNotFoundException问题_spring boot arthas_linyb极客之路的博客-CSDN博客
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。