You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java 17动态加载JAR包兼容问题(从Java 8迁移)

Java 8迁移至Java 17:运行时加载JAR包的类加载器兼容改造方案

问题背景

原Java 8代码通过强转系统类加载器为URLClassLoader并反射调用addURL方法实现运行时加载JAR包,迁移至Java 17后抛出如下异常:

java.lang.ClassCastException: class jdk.internal.loader.ClassLoaders$AppClassLoader cannot be cast to class java.net.URLClassLoader (jdk.internal.loader.ClassLoaders$AppClassLoader and java.net.URLClassLoader are in module java.base of loader 'bootstrap')

原因是JDK 9及后续版本为实现模块系统重构了类加载器:应用类加载器不再是URLClassLoader的实例,而是内部类jdk.internal.loader.ClassLoaders$AppClassLoader,同时JDK的模块封装机制限制了对内部API的直接访问。

解决方案

方案一:反射兼容系统类加载器(临时过渡)

通过反射直接调用应用类加载器的addURL方法,无需强转类型,但需注意Java 17的模块封装限制,需添加VM启动参数。

改造后的addURL方法:

public static void addURL(URL u) throws IOException {
    ClassLoader sysLoader = ClassLoader.getSystemClassLoader();
    boolean urlExists = false;

    // 检查URL是否已存在
    try {
        Method getURLsMethod = sysLoader.getClass().getMethod("getURLs");
        URL[] existingUrls = (URL[]) getURLsMethod.invoke(sysLoader);
        for (URL url : existingUrls) {
            if (url.toString().equalsIgnoreCase(u.toString())) {
                Log.info.log(new LogDetail().toBuilder()
                        .fileName(MethodHandles.lookup().lookupClass().getName())
                        .message("URL " + u + " is already in the CLASSPATH")
                        .build().buildLog());
                urlExists = true;
                break;
            }
        }
    } catch (Exception e) {
        Log.warn.log(new LogDetail().toBuilder()
                .fileName(MethodHandles.lookup().lookupClass().getName())
                .message("Failed to check existing URLs: " + ExceptionUtils.getMessage(e))
                .build().buildLog());
    }

    if (urlExists) {
        return;
    }

    // 反射调用addURL方法
    try {
        Method addURLMethod = sysLoader.getClass().getDeclaredMethod("addURL", URL.class);
        addURLMethod.setAccessible(true);
        addURLMethod.invoke(sysLoader, u);
    } catch (Throwable t) {
        throw new IOException("Error, could not add URL to system classloader", t);
    }
}

注意事项:Java 17中需添加VM启动参数以允许反射访问内部API:

--add-opens java.base/jdk.internal.loader=ALL-UNNAMED

该方案依赖JDK内部API,未来版本可能失效,仅适合临时过渡。

方案二:自定义URLClassLoader(推荐)

创建自定义的URLClassLoader来加载指定目录下的JAR包,完全规避系统类加载器的内部变更,符合Java模块化规范。

改造后的ClasspathUtils类:

import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import com.immunesecurity.shared.lib.Log;
import com.immunesecurity.shared.lib.LogDetail;
import org.apache.commons.lang3.exception.ExceptionUtils;

public class ClasspathUtils {
    private static URLClassLoader customClassLoader;

    static {
        // 初始化自定义类加载器,父加载器设为系统类加载器以继承现有类路径
        try {
            customClassLoader = new URLClassLoader(new URL[0], ClassLoader.getSystemClassLoader());
        } catch (Exception e) {
            Log.error.log(new LogDetail().toBuilder()
                    .fileName(MethodHandles.lookup().lookupClass().getName())
                    .message("Failed to initialize custom class loader: " + ExceptionUtils.getMessage(e))
                    .build().buildLog());
        }
    }

    /**
     * Adds the jars in the given directory to custom classpath
     * 
     * @param directory
     * @throws IOException
     */
    public static void addDirToClasspath(File directory) throws IOException {
        if (directory.exists()) {
            // 仅过滤JAR文件
            File[] files = directory.listFiles((dir, name) -> name.toLowerCase().endsWith(".jar"));
            if (files != null) {
                for (File file : files) {
                    addURL(file.toURI().toURL());
                }
            }
        } else {
            Log.warn.log(new LogDetail().toBuilder()
                    .fileName(MethodHandles.lookup().lookupClass().getName())
                    .methodName(Thread.currentThread().getStackTrace()[1].getMethodName())
                    .lineNumber(Thread.currentThread().getStackTrace()[1].getLineNumber())
                    .threadName(Thread.currentThread().getName())
                    .message("The directory \"" + directory + "\" does not exist!")
                    .build().buildLog());
        }
    }

    /**
     * Add URL to custom classpath
     * 
     * @param u URL
     * @throws IOException IOException
     */
    public static void addURL(URL u) throws IOException {
        // 检查URL是否已存在于自定义类加载器中
        URL[] existingUrls = customClassLoader.getURLs();
        for (URL url : existingUrls) {
            if (url.toString().equalsIgnoreCase(u.toString())) {
                Log.info.log(new LogDetail().toBuilder()
                        .fileName(MethodHandles.lookup().lookupClass().getName())
                        .message("URL " + u + " is already in the custom classpath")
                        .build().buildLog());
                return;
            }
        }

        // 反射调用URLClassLoader的addURL方法(该方法为protected)
        try {
            Method addURLMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            addURLMethod.setAccessible(true);
            addURLMethod.invoke(customClassLoader, u);
        } catch (Throwable t) {
            throw new IOException("Error, could not add URL to custom classloader", t);
        }
    }

    /**
     * Load class using custom class loader
     * 
     * @param className full class name
     * @return loaded class
     * @throws ClassNotFoundException if class not found
     */
    public static Class<?> loadClass(String className) throws ClassNotFoundException {
        return customClassLoader.loadClass(className);
    }
}

使用说明:后续需要加载JAR中的类时,需调用ClasspathUtils.loadClass("com.example.TargetClass"),而非默认的类加载器。该方案稳定可控,不受JDK内部类加载器变更影响。

方案对比

方案优点缺点
反射兼容系统类加载器无需修改现有类加载逻辑依赖内部API,Java 17需额外VM参数,未来版本可能失效
自定义URLClassLoader稳定合规,不受JDK版本变更影响需要修改现有代码中加载类的逻辑

内容的提问来源于stack exchange,提问作者SudeepShakya

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 22:07:05