异常情况
在springboot项目启动后,调用某个接口时产生如下报错:
java.lang.RuntimeException:
Socket Factory class not found:
java.lang.ClassNotFoundException:
Class org.apache.hadoop.net.StandardSocketFactory not found
代码片段如下:
CompletableFuture.runAsync(() -> {
queryInventorySinkToHdfs(); // 这行的具体方法报错
});
然后就开始下面的排查过程
确认class是否存在
在bin目录下找到相应的jar包
之后解压查看
// 调用命令进行解压lib到当前目录下
jar xf schedule-task.jar BOOT-INF/lib/
确认StandardSocketFactory.class 存在,排除 jar包版本问题
类加载器
这时候我们开始怀疑类加载器的问题
打断点在IDEA进行远程debug:
到报错处发现:
这时候我们应该察觉到一些异常
正常来讲,我们的 springBoot 项目的类加载器应该为 LaunchedURLClassLoader
,同时在 Tomcat 嵌入后,加载器会变成其子类 TomcatEmbeddedWebappClassLoader
,而不应该是 AppClassLoader
问题的结果已经明了,我们来探寻下为什么类加载器会发生变化?
ForkJoinPool线程
在主线程提交任务后,我们理所当然的认为执行任务的线程会继承主线程的 上下文类加载器(context class loader)
CompletableFuture.runAsync(() -> {
queryInventorySinkToHdfs();
});
但是上面的问题说明并不是,我们在去掉异步后发现其类加载器变为了 TomcatEmbeddedWebappClassLoader
,同时报错消失。
这就说明了主线程在提交任务到ForkJoinPool 线程池时,任务线程并没有继承主线程的 上下文类加载器(context class loader)
我们结合 源码来看下:
CompletableFuture.runAsync()方法如下:
继续找到这个异步线程池:
一般为多个,即调用的是ForkJoinPool.commonPool()
这时候来到 ForkJoinPool 里面:
进一步看下 common:
发现是在静态代码块里初始化的
继续看下这个初始化过程:
没有看到指定类加载器的过程
所以会沿用默认的上下文类加载器 AppClassLoader。在大多数Java应用中,系统类加载器是默认的类加载器,用于加载应用程序的类和资源。
解决方案
手动设置类加载器
获取主线程的类加载器之后,在每次任务执行时手动设置
// 获取当前线程的类加载器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
CompletableFuture.runAsync(() -> {
// 在新线程中设置类加载器
Thread.currentThread().setContextClassLoader(classLoader);
queryInventorySinkToHdfs();
});
采用自定义线程池
线程池在初始化的时候设置下线程的类加载器即可
最终问题得到解决,同时也进一步了解了类加载等知识。