ForkJoinPool/CompletableFuture/Stream.parallel中使用OpenFeign:解决ClssNotFound

问题的理解

在使用Java 11进行开发时,当结合ForkJoinPool和OpenFeign使用时,会出现一个特定的问题,表现为“ClassNotFound”的错误。这个问题的根源在于几个因素:

  1. Spring Boot Fat JAR打包:在Spring Boot应用程序中,一个常见的打包格式是‘fat jar’。这种打包方式由于jar包结构的特殊性,以及用途的不同,导致类和资源的加载方式有所不同。在fat jar中,类和资源不是直接存储在jar包的顶层或指定的classpath下,而是存在于BOOT-INF/lib/BOOT-INF/classes/目录中。这种结构意味着标准的类加载器,如AppClassLoader,可能无法直接访问这些类和资源。

  2. Feign的懒加载特性:Feign作为一个声明式的Web服务客户端,其特点之一是懒加载。即,Feign客户端在程序启动时不会立即加载相关的类到内存中。这种懒加载机制在某些情况下可能导致类加载问题。

  3. 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应用程序时对类加载器行为的更多控制。

参考链接