前阵子业务部门的项目出现了一个很奇怪的问题,有个class明明存在,本地idea运行也没问题,然后一发布线上就出现ClassNotFoundException问题,而且线上这个class确实是存在的。本文就通过一个demo示例来复现这么一个情况
注: 本文的项目框架为springboot2。本文仅演示ClassNotFoundException相关内容,并不模拟业务流
业务服务A
- package com.example.helloloader.service;
-
- import org.springframework.stereotype.Service;
-
- @Service
- public class HelloService {
-
- public String hello(){
- return "hello loader";
- }
- }
- 复制代码
组件B
- @Component
- public class HelloServiceLoaderUtils implements ApplicationContextAware {
-
- private ApplicationContext applicationContext;
-
- public String invoke(String className){
- try {
- ClassLoader classLoader = ClassLoader.getSystemClassLoader();
- Class clz = classLoader.loadClass(className);
- Object bean = applicationContext.getBean(clz);
- Method method = clz.getMethod("hello");
- return (String) method.invoke(bean);
-
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- return null;
- }
-
-
- @Override
- public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
- this.applicationContext = applicationContext;
- }
- }
-
- 复制代码
服务A调用组件B
- @SpringBootApplication(scanBasePackages = "com.example")
- public class HelloLoaderApplication implements ApplicationRunner {
-
- @Autowired
- private HelloServiceLoaderUtils helloServiceLoaderUtils;
-
- public static void main(String[] args) {
- SpringApplication.run(HelloLoaderApplication.class, args);
- }
-
- @Override
- public void run(ApplicationArguments args) throws Exception {
- System.out.println(helloServiceLoaderUtils.invoke(HelloService.class.getName()));
- }
- }
-
- 复制代码
如果通过本地idea进行调用,控制台会正常打印出
- hello loader
- 复制代码
将业务服务A打包,通过
- java -jar hello-loader-0.0.1-SNAPSHOT.jar
- 复制代码
启动访问

出现了ClassNotFoundException异常
class存在,却找不到class,要么就是类加载器错了,要么是class的位置错了。因此通过arthas进行排查。对arthas不了解的朋友,可以查看如下文章 java应用线上诊断神器--Arthas
我们通过如下命令查看com.example.helloloader.service.HelloService加载器
- sc -d com.example.helloloader.service.HelloService
- 复制代码

从图片可以看出打包后的HelloService的类加载器为spring封装过的加载器,因此用appClassLoader是加载不到HelloService
1、方法一将appClassLoader改成spring封装的加载器
做法就是将ClassLoader.getSystemClassLoader()改成 Thread.currentThread().getContextClassLoader()即可
改好重新打包。此时重新运行,观察控制台
- 当前类加载器:org.springframework.boot.loader.LaunchedURLClassLoader@439f5b3d
- hello loader
- 复制代码
2、方法二修改打包方式
将打包插件由
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-maven-pluginartifactId>
- plugin>
- plugins>
- build>
- 复制代码
切换成
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.pluginsgroupId>
- <artifactId>maven-jar-pluginartifactId>
- <version>2.6version>
- <configuration>
- <archive>
- <manifest>
- <addClasspath>trueaddClasspath>
- <classpathPrefix>lib/classpathPrefix>
- <mainClass>com.example.helloloader.HelloLoaderApplicationmainClass>
- manifest>
- archive>
- configuration>
- plugin>
- <plugin>
- <groupId>org.apache.maven.pluginsgroupId>
- <artifactId>maven-dependency-pluginartifactId>
- <version>2.10version>
- <executions>
- <execution>
- <id>copyid>
- <phase>packagephase>
- <goals>
- <goal>copy-dependenciesgoal>
- goals>
- <configuration>
- <outputDirectory>
- ${project.build.directory}/lib
- outputDirectory>
- configuration>
- execution>
- executions>
- plugin>
- plugins>
- build>
- 复制代码
切换打包方式后,重新运行
- 当前类加载器:sun.misc.Launcher$AppClassLoader@55f96302
- hello loader
- 复制代码
此时正常输出,且加载器为AppClassLoader。我们可以通过
- sc -d com.example.helloloader.service.HelloService
- 复制代码
观察HelloService的类加载器

此时的HelloService的类加载器为AppClassLoader
1、如果项目采用springboot的打包插件,他的class会放在/BOOT-INF,且该目录下的class类加载器为
- org.springframework.boot.loader.LaunchedURLClassLoader
- 复制代码
2、arthas是个好东西,谁用谁知道
3、当时业务排查的时候,过程是比我文章示例还要复杂一点。因为项目是部署到k8s中,当本地项目启动没问题时,业务方的研发就一直把问题聚焦在k8s中,一直觉得是k8s引发的问题。
后面他们业务方找到我,叫我帮忙排查,我第一反应就是可能打包出了问题,于是我让业务方打个包,本地以java -jar试下,但业务方的研发又很肯定的说,他试过本地打jar运行也没问题。因为业务方的代码我这边是没权限访问的,没办法进行验证。后面只能建议他们安装arthas,最终结合arthas解决了问题