CompletableFuture的默认线程池ForkJoinPool会导致类加载失败

1、相关代码块

使用静态代码块初始化策略模式

java 复制代码
public class PreviewHandlerFactory {
    public static final Map<String, BasePreviewTypeAbstractHandler> PREVIEW_TYPE_MAP = new HashMap<>();

    static {
        initPreviewTypeHandler();
    }

    private static void initPreviewTypeHandler() {
        Class clazz = BasePreviewTypeAbstractHandler.class;
        Set<Class<?>> classes = null;
        try {
            classes = ScanSupport.classInfos(clazz.getPackage().getName());
        } catch (Exception e) {
            log.error("扫描PreviewTypeAbstractHandler失败:", e);
        }
        if (CollectionUtils.isNotEmpty(classes)) {
            for (Class<?> aClass : classes) {
                if (ScanSupport.validate(aClass.getSuperclass(), clazz)) {
                    try {
                        BasePreviewTypeAbstractHandler handler = (BasePreviewTypeAbstractHandler) aClass.newInstance();
                        PREVIEW_TYPE_MAP.put(handler.type(), handler);
                    } catch (Exception e) {
                        log.error("初始化PreviewTypeAbstractHandler失败:", e);
                    }
                }
            }
        }else {
            log.error("未扫描PreviewTypeAbstractHandle相关处理器");
        }

    }

    public static BasePreviewTypeAbstractHandler getPreviewTypeHandler(PreviewTypeEnum type) throws Exception {
        BasePreviewTypeAbstractHandler previewTypeHandler = PREVIEW_TYPE_MAP.get(type.getName());
        if (previewTypeHandler == null) {
            throw new Exception("预览类型异常");
        }
        return previewTypeHandler;
    }
}

使用CompletableFuture的默认线程池调用getPreviewTypeHandler(PreviewTypeEnum type)方法触发静态代码块进行指定类加载。

java 复制代码
 ClassLoader contextClassLoader1 = Thread.currentThread().getContextClassLoader();
        log.info("contextClassLoader1-----" + contextClassLoader1.toString());
        CompletableFuture.runAsync(() -> {
            try {
                ClassLoader contextClassLoader2 = Thread.currentThread().getContextClassLoader();
                log.info("contextClassLoader2-----" + contextClassLoader2.toString());
                PreviewHandlerFactory.getPreviewTypeHandler(PreviewTypeEnum.PREVIEW_TYPE_ORIGINAL);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

2、现象

在本地使用idea运行程序,一切正常,可以扫描到预期的类。

上测试环境后,出现问题,扫描不到预期的类。

3、原因分析

本地启动

在idea点击运行按钮运行程序,实际上idea是javac编译后用java命令classpath参数把依赖路径全部都加入命令行。

它的所有依赖类加载器都是使用的是TomcatEmbeddedWebappClassLoader,他的父加载器是AppClassLoader(它可以加载classpath路径下的所有类);

并且在使用CompletableFuture 时,如果不指定自定义线程池,则会使用默认的线程池 ForkJoinPool,在默认的线程工厂中设置了创建线程的时候使用Application ClassLoader(应用程序类加载器) ,可以加载classpath下的类

java -jar方式启动

springboot项目打包后在测试环境上的启动方式是java -jar xxx.jar。

相关结构:

问题来了:普通的 Application ClassLoader 能加载 BOOT-INF/classes/MyApp.class 吗?

不能! 因为标准 ClassLoader 只会从 jar 根目录找类,根本不知道 BOOT-INF 是啥。

Spring Boot 怎么解决的?

它自己实现了一套 LaunchedURLClassLoader,继承自 URLClassLoader,专门用来加载 BOOT-INF 里的内容。

关键逻辑在 org.springframework.boot.loader.LaunchedURLClassLoader:

它会把 BOOT-INF/classes 和 BOOT-INF/lib/*.jar 全部加入自己的搜索路径;

当需要加载 com.example.MyService 时,它直接去 BOOT-INF/classes/com/example/MyService.class 找;

但它依然遵守"双亲委派"!

也就是说:Spring Boot 并没有打破双亲委派,而是在 Application ClassLoader 之下,又加了一层自定义加载器,继续遵守双亲委派规则。

所以会出现CompletableFuture 有些类无法加载(因为默认的CommonJoinPool设置了类加载器为AppClassLoader,无法访问 BOOT-INF/ 路径),委派给父类也无法加载。

LaunchedURLClassLoader加载器是唯一能够加载 BOOT-INF/下类的类加载器。

解决方案

任务内显式指定类加载器

java 复制代码
ClassLoader contextClassLoader1 = Thread.currentThread().getContextClassLoader();
        log.info("contextClassLoader1-----" + contextClassLoader1.toString());
        CompletableFuture.runAsync(() -> {
            try {
                Thread.currentThread().setContextClassLoader(contextClassLoader1);
                ClassLoader contextClassLoader2 = Thread.currentThread().getContextClassLoader();
                log.info("contextClassLoader2-----" + contextClassLoader2.toString());
                PreviewHandlerFactory.getPreviewTypeHandler(PreviewTypeEnum.PREVIEW_TYPE_ORIGINAL);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

使用自定义线程池

java 复制代码
ClassLoader contextClassLoader1 = Thread.currentThread().getContextClassLoader();
        log.info("contextClassLoader1-----" + contextClassLoader1.toString());
        CompletableFuture.runAsync(() -> {
            try {
//                Thread.currentThread().setContextClassLoader(contextClassLoader1);
                ClassLoader contextClassLoader2 = Thread.currentThread().getContextClassLoader();
                log.info("contextClassLoader2-----" + contextClassLoader2.toString());
                PreviewHandlerFactory.getPreviewTypeHandler(PreviewTypeEnum.PREVIEW_TYPE_ORIGINAL);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }, PreviewTaskHandlerExecutors.executor);
相关推荐
JAVA面经实录91711 小时前
Java企业级工程化·终极完整版背诵手册(无遗漏、全覆盖、面试+落地通用)
java·开发语言·面试
许彰午13 小时前
CacheSQL(二):主从复制——OpLog 环形缓冲区与故障自动恢复
java·数据库·缓存
Bat U14 小时前
JavaEE|多线程初阶(七)
java·开发语言
掌心向暖RPA自动化17 小时前
如何获取网页某个元素在屏幕可见部分的中心坐标影刀RPA懒加载坐标定位技巧
java·javascript·自动化·rpa·影刀rpa
日取其半万世不竭17 小时前
Minecraft Java版社区服务器搭建教程(Linux,适合新手)
java·linux·服务器
TeamDev17 小时前
JxBrowser 9.0.0 版本发布啦!
java·前端·混合应用·jxbrowser·浏览器控件·跨平台渲染·原声输入
AI人工智能+电脑小能手18 小时前
【大白话说Java面试题】【Java基础篇】第24题:Java面向对象有哪些特征
java·开发语言·后端·面试
AI人工智能+电脑小能手18 小时前
【大白话说Java面试题】【Java基础篇】第25题:JDK1.8的新特性有哪些
java·开发语言·后端·面试
likerhood19 小时前
SLF4J: Failed to load class “StaticLoggerBinder“ 解决
java·log4j·maven
早日退休!!!19 小时前
大模型推理瓶颈七层分析模型
java·服务器·数据库