Java 9应用中如何通过类加载器编程加载数据库Jar至类路径?
我之前升级Java版本的时候也踩过这个坑!Java 9引入模块化系统后,系统类加载器(Application ClassLoader)的实现不再继承URLClassLoader了,所以你之前那种强制类型转换的写法肯定会抛出ClassCastException,太坑了😅。
为什么旧代码失效?
在Java 8及之前,系统类加载器是URLClassLoader的子类,直接强转后调用addURL方法没问题。但Java 9开始,系统类加载器换成了jdk.internal.loader.ClassLoaders$AppClassLoader,它继承自BuiltinClassLoader,和URLClassLoader没有继承关系,强转自然失败。
适配Java 9的两种方案
方案1:反射调用内部的addURL方法(兼容旧代码)
虽然系统类加载器不再是URLClassLoader,但它的父类BuiltinClassLoader依然提供了addURL方法(只是访问权限是包私有),我们可以通过反射来调用它,实现和旧代码一样的效果:
import java.lang.reflect.Method; import java.net.URL; public class JarLoader { public static void addJarToClasspath(URL jarUrl) throws Exception { // 获取系统类加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); // 递归查找addURL方法(遍历类继承链) Method addUrlMethod = findAddUrlMethod(systemClassLoader.getClass()); addUrlMethod.setAccessible(true); // 调用方法添加Jar路径 addUrlMethod.invoke(systemClassLoader, jarUrl); } private static Method findAddUrlMethod(Class<?> clazz) throws NoSuchMethodException { try { // 先尝试当前类的addURL方法 return clazz.getDeclaredMethod("addURL", URL.class); } catch (NoSuchMethodException e) { // 当前类没有就递归找父类 Class<?> superClass = clazz.getSuperclass(); if (superClass == null) { throw e; } return findAddUrlMethod(superClass); } } }
这种方式不需要修改太多旧代码,能快速适配Java 9+,但要注意:这依赖JDK的内部实现,未来Java版本如果修改了addURL的定义,可能会失效,属于临时兼容方案。如果是Java 16+,还需要在启动参数里添加--add-opens java.base/jdk.internal.loader=ALL-UNNAMED,绕过模块封装规则的限制。
方案2:使用模块化API加载(推荐新应用)
如果你的应用已经迁移到Java模块化系统,推荐用ModuleLayer来动态加载模块化Jar,这种方式更符合Java 9+的设计规范:
import java.net.URI; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Set; import java.util.stream.Collectors; public class ModuleJarLoader { public static void loadModularJar(URL jarUrl) throws Exception { // 将Jar URL转换为Path对象 Path jarPath = Paths.get(new URI(jarUrl.toString())); // 基于启动模块层创建新的模块查找器 ModuleLayer parentLayer = ModuleLayer.boot(); ModuleFinder moduleFinder = ModuleFinder.of(jarPath); // 提取Jar中的模块名称 Set<String> moduleNames = moduleFinder.findAll().stream() .map(ref -> ref.descriptor().name()) .collect(Collectors.toSet()); // 解析并加载模块到新的模块层 Configuration config = parentLayer.configuration() .resolve(moduleFinder, ModuleFinder.of(), moduleNames); ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); ModuleLayer newLayer = parentLayer.defineModulesWithOneLoader(config, systemClassLoader); // 验证模块加载结果 newLayer.modules().forEach(module -> System.out.println("成功加载模块:" + module.getName())); } }
这种方式更规范,但只适合模块化的Jar;如果是非模块化的普通Jar,还是方案1更直接。
内容的提问来源于stack exchange,提问作者user3375401




