目标
在Forge扫描Mod之前将 它的依赖下载并动态注入到ClassLoader,而不是shadow打包FatJar,目的是为了减小Mod的体积。下载部分不再阐述,本文只探讨动态注入部分
思路
因为需要在Forge扫描Mod之前注入(如果在Mod中写动态注入,那么注入代码只可能在Forge扫描Mod结束后才能运行,而这时已经因为找不到依赖报CFE了),所以此Mod必须是CoreMod,也正是因为此Mod只能是CoreMod,我们无法使用Mixin这样便捷的工具(CoreMod和Mixin是同一优先级的,而且如果要想在自己的Mod内使用Mixin,比如等待FML扫完Mod后,那时候类已经被加载,因为找不到依赖已经CFE了)对类进行操作,我们不可避免的要写Java字节汇编码
但若熟悉Forge Mod加载流程的读者这时可能又会发现一个问题,Forge的启动流程是(MC 1.13.2+,在这之前是LaunchWrapper引导启动,在1.13.2后cpw将其改为ModLauncher引导,启动逻辑可能有区别):
- ModLauncher启动,各继承了ILaunchPlugin(没有外部接口注册,但是有一些开发者为了追求极早的启动可以通过反射进行注册)的早期启动插件启动
- ModLauncher挂载的各CoreMod启动(不分先后)
- Mixin SubSystem启动并开始增强原版类
- 其他CoreMod启动(准确地说,继承了ITransformationService的CoreMod和使用JS接口的CoreMod,后者运行在Nashorn隔离沙箱内,功能极少,我们不考虑)
- 启动forgeClient/forgeServer
- FML挂载Mod事件总线,开始扫描Mod,读取Mod依赖,版本配置等信息
- MinecraftForge开始加载,挂载Forge事件总线,启动生命周期管理,完成方块和物品等内容的注册
- Mixin开始增强Mod类
- ...
我们预想的方案是在2.2注册我们自己的CoreMod完成依赖注入,但这是不可能的,因为我们需要增强的类是ModLauncher的ClassLoader(TransformingClassLoader/ModuleClassLoader),但是这些ClassLoader在1中就已经实例化并启动了,我们无法在2.2中的CoreMod接口中扫到它(或者说,提供扫描的就是它俩),因此我们必须考虑HotSwap,即热交换这些ClassLoader的代码,也就是进行动态代理
到这里我们的思路就很明确了:
- 注册我们的CoreMod(毕竟还是要在Forge启动前做注入)
- 在CoreMod调用转换类回调的时候,用ASM工具搓字节码增强ModuleClassLoader(TransformingClassLoader是它的子ClassLoader)
- 热交换替换到老的ModuleClassLoader
- 新的ModuleClassLoader开始加载我们的外部依赖
- 等到Mod扫描时,它已经可以扫描到我们事先加载的依赖项
开始实现!
热交换
首先我们探讨热交换的实现,要实现动态的热交换,唯一的选项就是java.lang.instrument.Instrumentation这个魔法类,它是javaagent主要用到的类之一,提供了动态的类热交换方法,被广泛的用于各字节码增强工具(比如ByteBuddy)中。要获取这个类的实例只能通过javaagent从外部获取,但是这并不符合我们想在自己的代码中调用的逻辑,我们不可能每次都挂一个javaagent上去,所以必须要通过其他方法获取到Instrumentation的实例
一种方法是:
- 动态的创建一个jar文件,里面是我们的javaagent代码,用javaagent的方法获取到
Instrumentation的实例 - 在运行时动态的将这个javaagent attach到我们的JVM虚拟机上
- 反射调用这个javaagent中的方法,获取到
Instrumentation
于是我们便使用了JvmHacker这个工具,它通过动态生成一个javaagent,并通过com.sun.tools.VirtualMachine attach到我们的虚拟机进程上来获取instrumentation的实例。此外该工具还内置了获取JDK底层魔法类sun.misc.Unsafe(JDK底层万能钥匙类,非公开接口,因为提供了可以无视权限访问字段的方法被广泛应用于各大需要高性能反射的库中)和魔法字段IMPL_LOOKUP(JDK底层万能Method Handle,可无视任何权限访问函数),我们这里只使用JvmHacker获取Instrumentation
在JDK8可以使用Unsafe直接拿到Instrumentation的实例,但是在JDK9+由于模块系统的引入,这种方法已经失效了,只能通过javaagent获取,所以我们不再讨论使用Unsafe获取底层的Instrumentation实现字段
之后我们调用Instrumentation的redefineClasses方法进行动态热交换
依赖隔离与双向访问
按照正常的JVM类加载双亲委派机制,子ClassLoader在加载类时会优先交予父ClassLoader加载,直至顶层ClassLoader,若仍未找到,再递归向下让子ClassLoader加载,双亲委派机制避免了Java内部类被错误的修改,但也导致了当前场景的一个问题——依赖冲突问题
假如Mod A打包了1.0版本的依赖,它会由ModuleClassLoader加载,若Mod B打包了2.0版本的依赖,它也会由ModuleClassLoader加载,假若1.0和2.0的包名类名都是相同但是实现不同,此时再引用这个库的依赖变为引发歧义,Mod A调用该库调用的可能是Mod B加载的2.0版本的实现,这就会导致未定义行为,为此我们必须自己设计一个ClassLoader打破Java的双亲委派机制,具体地
- 我们设计一个自己的ClassLoader,它优先加载我们的依赖
- 如果我们的依赖没找到,再去找父ClassLoader去尝试加载
这种由子到父的加载机制正好是和双亲委派机制的反着来的,在这种场景下,它可以解决我们的依赖冲突问题
java
import java.net.URL;
import java.net.URLClassLoader;
/**
* 桥接 ClassLoader
* 1. 从指定的 JAR URLs 加载类(重定向的依赖库)
* 2. 委托给 Thread Context ClassLoader 加载其他类
*/
public class BridgeClassLoader extends URLClassLoader {
// 防止循环委托的 ThreadLocal 标记
private static final ThreadLocal<Boolean> LOADING = ThreadLocal.withInitial(() -> false);
public BridgeClassLoader(URL[] urls) {
// 不设置 parent,完全由我们自己控制类加载逻辑
super(urls, null);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 检测重入(防止与 ModuleClassLoader 的循环委托)
if (LOADING.get()) {
// 已经在加载中,直接抛出异常,让上层处理
throw new ClassNotFoundException(name + " (prevented circular delegation)");
}
synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已经加载过
Class<?> c = findLoadedClass(name);
if (c != null) {
if (resolve) {
resolveClass(c);
}
return c;
}
// 2. 尝试从自己的 URLs 加载(重定向的依赖库)
try {
c = findClass(name);
if (resolve) {
resolveClass(c);
}
return c;
} catch (ClassNotFoundException ignored) {
// 继续尝试其他方式
}
// 3. 委托给 Thread Context ClassLoader(runtime-forge 模块的类)
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
if (contextLoader != null && contextLoader != this) {
// 设置重入标记
LOADING.set(true);
try {
c = contextLoader.loadClass(name);
if (resolve) {
resolveClass(c);
}
return c;
} catch (ClassNotFoundException ignored) {
// 继续尝试其他方式
} finally {
// 清除重入标记
LOADING.set(false);
}
}
// 4. 最后尝试 Platform ClassLoader(Java 平台类)
ClassLoader platformLoader = ClassLoader.getPlatformClassLoader();
if (platformLoader != null) {
c = platformLoader.loadClass(name);
if (resolve) {
resolveClass(c);
}
return c;
}
throw new ClassNotFoundException(name);
}
}
}
增强ModuleClassLoader
在正常情况下,我们上面新声明的BridgeClassLoader和ModLauncher真正加载类的ModuleClassLoader现在是没有关系的,ModuleClassLoader的子ClassLoader是TransformingClassLoader,在标准双亲委派模型中,父ModuleClassLoader找不到我们的依赖类,之后交予TransformingClassLoader加载,还是找不到我们的依赖类(因为我们的依赖是外来的,它不在TransformingClassLoader的加载范围内,它只负责CoreMod自己类的加载),这时候就要报错CFE了,所以我们必须把代码“插入”到ModuleClassLoader内,让它在查找我们的依赖类包名时直接去找我们的BridgeClassLoader加载,这样就能把我们的依赖类加载进来了,这也是我们需要编写字节汇编的地方
首先我们先编写一个接口方法,传入我们的依赖Jar文件列表,将加到BridgeClassLoader的classpath里,然后增强ModuleClassLoader并执行热交换
java
/**
* 初始化重定向类加载器,并使用 Instrumentation 动态修改 ModuleClassLoader
*
* @param jars 所有运行时依赖 JAR 文件
*/
public static void initializeAndRedefine(List<File> jars) {
try {
// 1. 创建 BridgeClassLoader
URL[] jarUrls = new URL[jars.size()];
for (int i = 0; i < jars.size(); i++) {
jarUrls[i] = jars.get(i).toURI().toURL();
}
BridgeClassLoader relocatedClassLoader = new BridgeClassLoader(jarUrls);
// 2. 收集所有需要拦截的包名前缀
List<String> managedPackages = new java.util.ArrayList<>();
for (DependencySpec spec : RuntimeEnv.getDependencies()) {
String packageToIntercept = null;
// 优先使用 toPackage(重定向后的包名)
if (spec.toPackage() != null) {
packageToIntercept = spec.toPackage();
}
// 否则使用 fromPackage
else if (spec.fromPackage() != null) {
packageToIntercept = spec.fromPackage();
}
if (packageToIntercept != null && !managedPackages.contains(packageToIntercept)) {
managedPackages.add(packageToIntercept);
}
}
// 3. 将包名列表和 BridgeClassLoader 存储到 System Properties
System.getProperties().put("runtime.classloader", relocatedClassLoader);
System.getProperties().put("runtime.packages", String.join(";", managedPackages));
// 4. 动态修改 ModuleClassLoader(注入加载逻辑)
redefineModuleClassLoader();
} catch (Exception e) {
throw new RuntimeException("无法初始化并修改 ModuleClassLoader", e);
}
}
这里我们考虑到了依赖的重定向,这里后面再说
因为后面需要获取BridgeClassLoader的实例,由于JDK9+模块系统的安全限制ModuleClassLoader是不能直接访问我们的BridgeClassLoader的,所以我们取了个巧将其存入SystemProperties,System类是java自己的类,ModuleClassLoader可以访问到
之后我们使用Instrumentation热交换ModuleClassLoader
java
/**
* 使用 Instrumentation 动态修改 ModuleClassLoader 的字节码
*/
private static void redefineModuleClassLoader() throws Exception {
String className = "cpw.mods.cl.ModuleClassLoader";
Class<?> moduleClassLoaderClass = Class.forName(className);
// 读取原始字节码
String classFilePath = className.replace('.', '/') + ".class";
byte[] originalBytes;
try (InputStream is = moduleClassLoaderClass.getClassLoader().getResourceAsStream(classFilePath)) {
if (is == null) {
throw new IllegalStateException("无法找到 ModuleClassLoader 的字节码文件");
}
originalBytes = is.readAllBytes();
}
// 修改字节码
byte[] modifiedBytes = transformClassBytes(originalBytes);
// 使用 Instrumentation 重新定义类
JvmHacker.instrumentation().redefineClasses(
new ClassDefinition(moduleClassLoaderClass, modifiedBytes)
);
}
我们增强的是ModuleClassLoader的loadClass(String, boolean)方法,它返回一个java Class对象,故其签名为(Ljava/lang/String;Z)Ljava/lang/Class;
- String:签名java.lang.String
- boolean:签名Z
- 返回:签名java.lang.Class
java
/**
* 使用 ASM 修改 ModuleClassLoader 的字节码,在 loadClass() 方法开头注入加载逻辑
*/
private static byte[] transformClassBytes(byte[] classBytes) {
ClassReader reader = new ClassReader(classBytes);
ClassNode classNode = new ClassNode();
reader.accept(classNode, 0);
// 同时修改两个 loadClass() 方法
int modifiedCount = 0;
for (MethodNode method : classNode.methods) {
if ("loadClass".equals(method.name)) {
// loadClass(String)
if ("(Ljava/lang/String;)Ljava/lang/Class;".equals(method.desc)) {
injectLoadClassHook(method);
modifiedCount++;
}
// loadClass(String, boolean)
else if ("(Ljava/lang/String;Z)Ljava/lang/Class;".equals(method.desc)) {
injectLoadClassHook(method);
modifiedCount++;
}
}
}
if (modifiedCount == 0) {
throw new IllegalStateException("未找到任何 loadClass 方法!");
}
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
classNode.accept(writer);
return writer.toByteArray();
}
这里另外一个loadClass方法是多余的,它仅作为备选方案,实际情况下根本不会被inject
现在我们考虑增强这个loadClass方法,我们考虑在ModuleClassLoader调用这个方法加载类之前,就先用我们的BridgeClassLoader加载我们的依赖类进入,也就是说,我们需要在这个方法调用的开头插入我们自己的代码,而这里就需要我们手搓Java字节码了
ASM
我们考虑插进去这样一段代码
java
String packages = System.getProperty("runtime.packages");
if (packages != null) {
for (String pkg : packages.split(";")) {
if (name.startsWith(pkg)) {
ClassLoader loader = (ClassLoader) System.getProperties().get("runtime.classloader");
if (loader != null) {
return loader.loadClass(name);
}
break;
}
}
}
现在我们来考虑如何将其用ASM写出来
首先我们需要考虑loadClass(String, boolean)这个方法调用时会发生什么
- this指针被压入操作数栈并被放入局部变量表,索引0,slot1
- 第一个String参数被压入操作数栈并被放入局部变量表,索引1,slot2
- 第二个boolean参数被压入操作数栈并被放入局部变量表,索引2,slot3
- 代码执行,调用
INVOKEVIRTUAL操作码 - 结果从栈顶出栈,this指针和参数出栈
- 结束本次调用
我们要在方法调用的开始插入我们的代码,首先我们要先获取packageStr变量,为此我们需要invoke getProperty方法
- 压入函数字符串常量
runtime.packages到操作数栈,翻译为指令 -> LDC "runtimes.packages" - 执行函数
getProperty(static静态方法) -> INVOKESTATIC getProperty,它隶属于java.lang.system,方法接受一个String参数返回一个String对象,所以签名为(Ljava/lang/String;)Ljava/lang/String; - 函数执行完毕,字符串常量弹出栈,结果入栈,现在栈顶指针指向slot4
- 将栈顶的结果保存到局部变量表
java
hook.add(new LdcInsnNode("runtime.packages"));
hook.add(new MethodInsnNode(
Opcodes.INVOKESTATIC,
"java/lang/System",
"getProperty",
"(Ljava/lang/String;)Ljava/lang/String;",
false
));
hook.add(new VarInsnNode(Opcodes.ASTORE, startSlot)); // packagesStr
接下来判断packageStr是否为空
- 从局部变量表中的
packageStr压入我们的packageStr到操作数栈 -> ALOAD startSlot - 如果为空我们就跳转到我们插入代码部分的结束前 -> IFNULL JUMP skipLabel
- 否则继续执行
java
hook.add(new VarInsnNode(Opcodes.ALOAD, startSlot));
hook.add(new JumpInsnNode(Opcodes.IFNULL, skipLabel));
之后我们split packageStr
- 压入
packageStr-> ALOAD startSlot - 压入常量
;-> LDC ";" - invoke split方法
packageStr;退栈,结果入栈- 将结果
package存入局部变量表
java
// String[] packages = packagesStr.split(";");
hook.add(new VarInsnNode(Opcodes.ALOAD, startSlot));
hook.add(new LdcInsnNode(";"));
hook.add(new MethodInsnNode(
Opcodes.INVOKEVIRTUAL,
"java/lang/String",
"split",
"(Ljava/lang/String;)[Ljava/lang/String;",
false
));
hook.add(new VarInsnNode(Opcodes.ASTORE, packagesSlot)); // packages
接下来我们写循环
- 定义循环变量,也就是整形字面量i,压入操作数栈 -> ICONST_0(-1~5时JVM使用优化的操作码ICONST而非LDC)
- 将i存入局部变量表,退栈 -> ISTORE iSlot
- 定义循环头loopStart label -> LABEL loopStart
- 判断循环是否终止
- 从局部变量表压
i入栈 -> ILOAD iSlot - 从局部变量表压
packages入栈 -> ALOAD packagesSlot - 压取数组长度操作码入栈 -> ARRAYLENGTH
- 执行整形比较,若大于等于则跳转到我们注入代码的结束,操作完毕后
packages,i退栈 -> ICMPGE JUMP skipLabel
- 从局部变量表压
- 执行循环
- i+1 -> IINC iSlot 1
- 跳转到循环头 -> GOTO loopStart
循环内
- 取
packages第i个元素- 压
packages入栈 -> ALOAD packagesSlot - 压
i入栈 -> ILOAD iSlot - 取数组元素 -> AALOAD
- 退栈存局部变量表 -> ASTORE pkgSlot
- 压
判断 if (name.startsWith(pkg))
- 压入
name-> ALOAD 1 - 压入
pkg-> ALOAD pkgSlot - 执行startsWith -> INVOKEVIRTUAL ...
- 如果返回false,也就是栈顶为0,则跳转到continue,也就是直接跳转到循环尾部 IFEQ ... JUMP labelContinue
执行 Object loaderObj = System.getProperties().get("runtime.classloader")
- 执行
getProperties(static静态方法),结果入栈 -> INVOKESTATIC ... - 压入字符串字面量 -> LDC "runtime.classloader"
- 执行
get,退栈前面的结果,入栈loaderObj
执行 if (loader(也就是loaderObj) != null) 判断
- 复制栈顶loaderObj一份 -> DUP
- 执行是否为空判断,loaderObj的副本退栈,如果为空,把剩下的一个loaderObj退栈,然后退出循环,因为循环后没有代码了,也就是跳转到我们注入代码的结束
skipLabel-> IFNULL JUMP skilWithPopLabel, skipLabel后是退栈操作 POP 和跳转 JUMP skipLabel
之后执行类型转换,loaderObj退栈,ClassLoader对象入栈 -> CHECKCAST ...
最后执行 return loader.loadClass(name):
- 压入
name-> ALOAD 1 - 执行loadClass方法,loader、name退栈,Class入栈 -> INVOKEVIRTUAL ...
- 栈顶返回Class,this指针和形参退栈,函数结束调用 -> ARETURN
代码:
java
/**
* 在 loadClass() 方法开头注入加载逻辑(完全内联,从 System Properties 读取)
* <p>
* 等价于:
* String packages = System.getProperty("runtime.packages");
* if (packages != null) {
* for (String pkg : packages.split(";")) {
* if (name.startsWith(pkg)) {
* ClassLoader loader = (ClassLoader) System.getProperties().get("runtime.classloader");
* if (loader != null) {
* return loader.loadClass(name);
* }
* break;
* }
* }
* }
*/
private static void injectLoadClassHook(MethodNode method) {
InsnList hook = new InsnList();
LabelNode skipLabel = new LabelNode();
LabelNode skipWithPopLabel = new LabelNode();
LabelNode loopStart = new LabelNode();
LabelNode loopContinue = new LabelNode();
// 计算起始局部变量槽位
// loadClass(String) -> slot 0: this, slot 1: name, 从 slot 2 开始
// loadClass(String, boolean) -> slot 0: this, slot 1: name, slot 2: resolve, 从 slot 3 开始
int startSlot = method.desc.equals("(Ljava/lang/String;)Ljava/lang/Class;") ? 2 : 3;
int packagesSlot = startSlot + 1;
int iSlot = startSlot + 2;
int pkgSlot = startSlot + 3;
// String packagesStr = System.getProperty("runtime.packages");
hook.add(new LdcInsnNode("runtime.packages"));
hook.add(new MethodInsnNode(
Opcodes.INVOKESTATIC,
"java/lang/System",
"getProperty",
"(Ljava/lang/String;)Ljava/lang/String;",
false
));
hook.add(new VarInsnNode(Opcodes.ASTORE, startSlot)); // packagesStr
// if (packagesStr == null) goto skip;
hook.add(new VarInsnNode(Opcodes.ALOAD, startSlot));
hook.add(new JumpInsnNode(Opcodes.IFNULL, skipLabel));
// String[] packages = packagesStr.split(";");
hook.add(new VarInsnNode(Opcodes.ALOAD, startSlot));
hook.add(new LdcInsnNode(";"));
hook.add(new MethodInsnNode(
Opcodes.INVOKEVIRTUAL,
"java/lang/String",
"split",
"(Ljava/lang/String;)[Ljava/lang/String;",
false
));
hook.add(new VarInsnNode(Opcodes.ASTORE, packagesSlot)); // packages
// int i = 0;
hook.add(new InsnNode(Opcodes.ICONST_0));
hook.add(new VarInsnNode(Opcodes.ISTORE, iSlot)); // i
// loop_start:
hook.add(loopStart);
// if (i >= packages.length) goto skip;
hook.add(new VarInsnNode(Opcodes.ILOAD, iSlot));
hook.add(new VarInsnNode(Opcodes.ALOAD, packagesSlot));
hook.add(new InsnNode(Opcodes.ARRAYLENGTH));
hook.add(new JumpInsnNode(Opcodes.IF_ICMPGE, skipLabel));
// String pkg = packages[i];
hook.add(new VarInsnNode(Opcodes.ALOAD, packagesSlot));
hook.add(new VarInsnNode(Opcodes.ILOAD, iSlot));
hook.add(new InsnNode(Opcodes.AALOAD));
hook.add(new VarInsnNode(Opcodes.ASTORE, pkgSlot)); // pkg
// if (!name.startsWith(pkg)) goto loop_continue;
hook.add(new VarInsnNode(Opcodes.ALOAD, 1)); // name
hook.add(new VarInsnNode(Opcodes.ALOAD, pkgSlot)); // pkg
hook.add(new MethodInsnNode(
Opcodes.INVOKEVIRTUAL,
"java/lang/String",
"startsWith",
"(Ljava/lang/String;)Z",
false
));
hook.add(new JumpInsnNode(Opcodes.IFEQ, loopContinue));
// 匹配成功,加载类
// Object loaderObj = System.getProperties().get("runtime.classloader");
hook.add(new MethodInsnNode(
Opcodes.INVOKESTATIC,
"java/lang/System",
"getProperties",
"()Ljava/util/Properties;",
false
));
hook.add(new LdcInsnNode("runtime.classloader"));
hook.add(new MethodInsnNode(
Opcodes.INVOKEVIRTUAL,
"java/util/Properties",
"get",
"(Ljava/lang/Object;)Ljava/lang/Object;",
false
));
// 栈: [Object]
// if (loaderObj == null) goto skip;
hook.add(new InsnNode(Opcodes.DUP)); // 复制一份,用于 null 检查
hook.add(new JumpInsnNode(Opcodes.IFNULL, skipWithPopLabel));
// 栈: [Object]
// ClassLoader loader = (ClassLoader) loaderObj;
hook.add(new TypeInsnNode(Opcodes.CHECKCAST, "java/lang/ClassLoader"));
// 栈: [ClassLoader]
// return loader.loadClass(name);
hook.add(new VarInsnNode(Opcodes.ALOAD, 1)); // name
hook.add(new MethodInsnNode(
Opcodes.INVOKEVIRTUAL,
"java/lang/ClassLoader",
"loadClass",
"(Ljava/lang/String;)Ljava/lang/Class;",
false
));
// 栈: [Class]
hook.add(new InsnNode(Opcodes.ARETURN));
// skip_with_pop: (栈上有一个 null Object,弹出后跳出循环)
hook.add(skipWithPopLabel);
hook.add(new InsnNode(Opcodes.POP)); // 弹出 null
hook.add(new JumpInsnNode(Opcodes.GOTO, skipLabel)); // 跳出循环
// loop_continue: i++, goto loop_start
hook.add(loopContinue);
hook.add(new IincInsnNode(iSlot, 1)); // i++
hook.add(new JumpInsnNode(Opcodes.GOTO, loopStart));
// skip: (继续原方法)
hook.add(skipLabel);
// 在方法开头插入
method.instructions.insert(hook);
}
CoreMod
最后我们继承一个ITransformationService的CoreMod类
java
import cpw.mods.modlauncher.api.IEnvironment;
import cpw.mods.modlauncher.api.IModuleLayerManager;
import cpw.mods.modlauncher.api.ITransformationService;
import cpw.mods.modlauncher.api.ITransformer;
import java.io.File;
import java.util.List;
import java.util.Set;
/**
* CoreMod
*/
public class Bootstrap implements ITransformationService {
@SuppressWarnings("all")
@Override
public String name() {
return "bootstrap";
}
@Override
public void initialize(IEnvironment environment) {
try {
// 下载并处理所有运行时依赖
List<File> runtimeJars = RuntimeDependencyLoader.initRuntimeLibs();
// 初始化重定向类加载器,并使用 Instrumentation 动态修改 ModuleClassLoader
ClassAppender.initializeAndRedefine(runtimeJars);
} catch (Throwable t) {
throw new RuntimeException("依赖加载失败", t);
}
}
@Override
@SuppressWarnings("all")
public void onLoad(IEnvironment env, Set<String> otherServices) {}
@SuppressWarnings("all")
@Override
public List<Resource> beginScanning(IEnvironment environment) {
return List.of();
}
@SuppressWarnings("all")
@Override
public List<Resource> completeScan(IModuleLayerManager layerManager) {
return List.of();
}
@SuppressWarnings("all")
@Override
public List<ITransformer> transformers() {
return List.of();
}
}
并在META-INF下将其注册为ModLauncher可以发现的服务,即可达到在ModLauncher启动时热交换ModuleClassLoader的功能
全部评论 (0)
暂无评论,快来抢沙发吧~