SpringBoot 2.4.2 ResourceLoader是否线程安全?并发抛FileNotFoundException如何解决
问题根因
该问题本质是线程上下文类加载器(TCCL)不一致导致的:
- Spring Boot 打包成jar运行时,会使用自定义的
LaunchedURLClassLoader加载BOOT-INF/classes下的类和资源、以及BOOT-INF/lib下的嵌套jar内容,只有这个类加载器能识别Spring Boot的嵌套jar结构。 - 应用主线程启动时,Spring Boot已经自动将主线程的TCCL设置为
LaunchedURLClassLoader,所以串行调用时读取资源正常。 - 并行流默认使用JVM全局共享的
ForkJoinPool.commonPool(),自定义线程池如果没有特殊配置,线程创建时的TCCL是系统类加载器,这两个类加载器都无法识别Spring Boot的嵌套jar结构,因此找不到classpath下的资源。 - 单元测试环境下类路径是扁平结构,没有嵌套jar,系统类加载器也可以直接读取到资源,因此无法复现问题。
解决方案
以下三个方案均可解决问题,推荐优先使用方案3:
方案1:强制使用当前Bean的类加载器加载资源
直接使用当前类的类加载器(该类由Spring加载,必然是LaunchedURLClassLoader)读取资源,完全不依赖线程的TCCL:
@Service @RequiredArgsConstructor public class BeanWithResourceLoader{ @SneakyThrows private String readResource() { return IOUtils.toString( // 强制使用当前类的类加载器读取资源 BeanWithResourceLoader.class.getResourceAsStream("/some_resource.json"), StandardCharsets.UTF_8 ); } }
方案2:为线程池配置正确的TCCL
如果需要保留ResourceLoader的使用逻辑,可以在自定义线程池、自定义ForkJoinPool的线程工厂中,统一为创建的线程设置Spring的类加载器:
@Bean public Executor customThreadPool() { ClassLoader springClassLoader = BeanWithResourceLoader.class.getClassLoader(); ThreadFactory threadFactory = runnable -> { Thread thread = new Thread(runnable); // 为新创建的线程设置正确的TCCL thread.setContextClassLoader(springClassLoader); return thread; }; // 并行流也可以使用该自定义线程工厂创建ForkJoinPool使用 return new ThreadPoolExecutor(10, 20, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), threadFactory); }
方案3:提前缓存静态资源内容
classpath下的资源一般是静态不变的,可以在Bean初始化阶段一次性加载到内存缓存,后续调用直接返回缓存值,既规避了类加载器问题,也减少了重复IO开销:
@Service public class BeanWithResourceLoader{ private String cachedResource; @PostConstruct @SneakyThrows public void initResource() { cachedResource = IOUtils.toString( BeanWithResourceLoader.class.getResourceAsStream("/some_resource.json"), StandardCharsets.UTF_8 ); } private String readResource() { return cachedResource; } }
内容的提问来源于stack exchange,提问作者Capacytron
相关产品推荐
相关产品推荐

