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
相关产品推荐
相关产品推荐

