如何在运行时屏蔽系统模块?实现类jlink的模块层配置
运行时创建裁剪后的系统模块层:可行方案详解
嘿,这个需求绝对可行!Java 9引入的模块系统其实已经内置了运行时动态创建自定义模块层的能力,完全能实现你想要的——类似jlink裁剪系统模块子集的效果,只不过是在程序运行时动态完成,而不是构建阶段。
核心思路
jlink的本质是在构建阶段打包JRE的模块子集,而我们要做的是通过ModuleLayer和Configuration API,在运行时动态构建一个只包含指定系统模块及其依赖的新模块层,以此实现对其他系统模块的屏蔽。
具体实现步骤
1. 确定需要的模块并处理依赖传递
首先你得明确要保留哪些系统模块,然后必须递归收集它们的所有依赖(包括requires transitive的隐式依赖),否则模块层初始化会失败。比如你要保留java.logging,就必须同时包含它依赖的java.base。
示例代码:
import java.util.*; import java.util.stream.Collectors; // 定义我们需要保留的核心系统模块 Set<String> targetModules = Set.of("java.base", "java.logging"); ModuleLayer bootLayer = ModuleLayer.boot(); Set<ModuleDescriptor> selectedDescriptors = new HashSet<>(); Queue<String> moduleQueue = new LinkedList<>(targetModules); // 递归收集所有依赖模块 while (!moduleQueue.isEmpty()) { String moduleName = moduleQueue.poll(); Module module = bootLayer.findModule(moduleName) .orElseThrow(() -> new IllegalArgumentException("模块不存在:" + moduleName)); ModuleDescriptor descriptor = module.getDescriptor(); if (selectedDescriptors.add(descriptor)) { // 添加该模块的所有非静态依赖(静态依赖一般是编译时的,运行时不需要强制包含) descriptor.requires().forEach(req -> { if (!req.isStatic() && selectedDescriptors.stream().noneMatch(md -> md.name().equals(req.name()))) { moduleQueue.add(req.name()); } }); } }
2. 构建自定义模块配置
接下来要基于引导层的配置,创建只包含我们筛选出的模块的新配置:
ModuleFinder systemModuleFinder = ModuleFinder.ofSystem(); Configuration bootConfig = bootLayer.configuration(); // 解析并绑定新配置:只包含我们筛选的模块,依赖从系统模块查找 Configuration trimmedConfig = bootConfig.resolveAndBind( systemModuleFinder, ModuleFinder.of(), // 这里可以添加自定义模块的查找器,不需要的话留空 selectedDescriptors.stream() .map(ModuleDescriptor::name) .collect(Collectors.toSet()) );
3. 创建新模块层与类加载器
最后一步是用新配置创建模块层,并用它的类加载器来加载类——这样就只能访问到我们指定的模块内容了:
// 父类加载器可以用平台类加载器,保证基础类的可访问性 ClassLoader parentClassLoader = ClassLoader.getPlatformClassLoader(); // 定义新模块层,父层设为引导层 ModuleLayer trimmedLayer = ModuleLayer.defineModules( trimmedConfig, List.of(bootLayer), // 父层列表,这里只需要引导层 module -> parentClassLoader // 为每个模块提供类加载器,这里统一用父类加载器 ).layer(); // 测试:从新模块层加载类 ClassLoader trimmedClassLoader = trimmedLayer.findModule("java.logging") .orElseThrow().getClassLoader(); Class<?> loggerClass = trimmedClassLoader.loadClass("java.util.logging.Logger"); Object logger = loggerClass.getMethod("getLogger", String.class).invoke(null, "TrimmedLayerLogger"); loggerClass.getMethod("info", String.class).invoke(logger, "运行时裁剪模块层成功!"); // 尝试加载被屏蔽的模块类,比如java.sql.DriverManager,会抛出ClassNotFoundException try { trimmedClassLoader.loadClass("java.sql.DriverManager"); } catch (ClassNotFoundException e) { System.out.println("成功屏蔽了java.sql模块:" + e.getMessage()); }
关键注意事项
- 依赖传递必须处理:如果遗漏了某个模块的依赖,模块层初始化会抛出
LinkageError,所以一定要递归收集所有依赖。 - 模块层的隔离性:新模块层的类加载器只能访问该层内的模块,引导层的其他模块会被完全屏蔽,实现了你要的效果。
- 静态依赖的处理:代码里跳过了静态依赖(
isStatic()为true的requires),因为静态依赖是编译时的可选依赖,运行时如果不需要可以不包含。 - 权限问题:如果你的应用启用了安全管理器,需要额外配置权限,但Java 17+安全管理器默认已禁用,所以一般无需担心。
内容的提问来源于stack exchange,提问作者Sormuras
相关产品推荐
相关产品推荐

