ForkJoinPool/CompletableFuture/Stream.parallel中使用OpenFeign:解决ClssNotFound
问题的理解
在使用Java 11进行开发时,当结合ForkJoinPool和OpenFeign使用时,会出现一个特定的问题,表现为“ClassNotFound”的错误。这个问题的根源在于几个因素:
-
Spring Boot Fat JAR打包:在Spring Boot应用程序中,一个常见的打包格式是‘fat jar’。这种打包方式由于jar包结构的特殊性,以及用途的不同,导致类和资源的加载方式有所不同。在fat jar中,类和资源不是直接存储在jar包的顶层或指定的classpath下,而是存在于
BOOT-INF/lib/
和BOOT-INF/classes/
目录中。这种结构意味着标准的类加载器,如AppClassLoader,可能无法直接访问这些类和资源。 -
Feign的懒加载特性:Feign作为一个声明式的Web服务客户端,其特点之一是懒加载。即,Feign客户端在程序启动时不会立即加载相关的类到内存中。这种懒加载机制在某些情况下可能导致类加载问题。
-
ForkJoinPool的类加载器问题:在Java 9之后,ForkJoinPool修复了一个bug,导致它创建的线程使用的SystemClassLoader是AppClassLoader。但由于上述提到的fat jar结构问题,AppClassLoader可能无法正确加载在
BOOT-INF/lib/
和BOOT-INF/classes/
中的类。
问题的复现
在实际应用中,这个问题可以通过以下方式复现:使用Stream.parallel(并行流,内部使用ForkJoinPool线程池)循环多次执行Feign客户端的调用。在这种情况下,可能会出现至少一次“ClassNotFound”的错误,尽管其他线程可能能够正确执行。
问题的原因
问题发生的根本原因是ForkJoinPool分配任务时可能会将任务分配给主线程。当主线程完成了Feign的懒加载后,后续的任务线程不需要再次加载Feign,因此可以直接执行Feign任务。但如果某个任务由一个没有完成Feign懒加载的线程执行,该线程可能无法找到所需的类,从而导致“ClassNotFoundException”。
解决方案
针对在使用Java 11、ForkJoinPool和OpenFeign时出现的类加载问题,有几种可能的解决方案。这些问题源于ForkJoinPool
在Java 9及以后版本中使用系统类加载器作为上下文类加载器,特别是在使用fat JAR运行应用程序时。
解决方案一:使用父ApplicationContext
的类加载器
一种解决方案是在子ApplicationContext
中使用父ApplicationContext
的类加载器。这可以通过在AnnotationConfigApplicationContext
实例化后设置类加载器来实现,例如:
context = new AnnotationConfigApplicationContext(beanFactory);
context.setClassLoader(this.parent.getClassLoader());
然而,这种方法有一个缺点:通常父ApplicationContext
不会显式地设置类加载器,而是使用ClassUtils.getDefaultClassLoader()
返回默认的类加载器。如果上下文类加载器已经被设置,那么这种实现将返回上下文类加载器。
解决方案二:自定义ApplicationContextInitializer
另一种解决方案是创建一个自定义的ApplicationContextInitializer
实现,并将其添加到spring.factories
中。这样可以手动设置启动应用程序的上下文类加载器。示例实现如下:
public class ClassLoaderApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.setClassLoader(applicationContext.getClassLoader());
}
}
这种方法通过确保应用程序的上下文类加载器与其类加载器一致,从而解决了由于类加载器不匹配导致的ClassNotFound的问题。
解决方案三:不使用ForkJoinPool
仅适用于CompletableFuture的情况,在提交任务时指定其他线程池。
总结
解决这类问题需要仔细考虑类加载器的使用方式,尤其是在使用fat JAR和复杂的多线程环境下。通过以上解决方案,可以有效地避免因类加载器不一致导致的“ClassNotFound”错误。此外,这些解决方案还提供了在构建复杂的Spring Boot应用程序时对类加载器行为的更多控制。