基于ASM实现类似Fabric Mixin:如何将字节码写回原方法?
问题描述
我正在尝试制作类似Minecraft Fabric模组加载器中的Mixin工具,目前已完成的部分似乎能正常运行(未抛出错误,但暂无其他验证方式),但现在卡在了需要将合并后的字节码写回原方法的步骤。请问该如何实现?
我的代码
/** * @param method1 基础方法 * @param method2 待注入的代码所属方法 * @param descriptor 两个方法的描述符 * @param access_level 两个方法的访问权限(私有、公开等) * @param pos 注入位置 * @param method1Class 基础方法所属的类 * @param method 基础方法对应的Method对象 * @throws Exception 出现异常时抛出 */ public static void mixin(MethodNode method1, MethodNode method2, String descriptor, int access_level, String pos, Class<?> method1Class, Method method) throws Exception { // 创建合并特征的新方法 MethodNode mergedMethod = new MethodNode( access_level, "mergedMethod", descriptor, null, null ); // 将两个方法的指令合并到新方法中 switch (pos){ case "HEAD" -> { mergedMethod.instructions.add(method2.instructions); mergedMethod.instructions.add(method1.instructions); } case "TAIL" -> { mergedMethod.instructions.add(method1.instructions); mergedMethod.instructions.add(method2.instructions); } default -> { throw new IllegalArgumentException("Invalid mixin point provided!"); } } // 重新计算栈大小和最大本地变量数 mergedMethod.maxStack = Math.max(method1.maxStack, method2.maxStack); mergedMethod.maxLocals = Math.max(method1.maxLocals, method2.maxLocals); // 生成合并方法的字节码 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); mergedMethod.accept(cw); byte[] bytecode = cw.toByteArray(); method.setAccessible(true); }
解决方案
要把合并后的字节码写回原方法,核心是通过底层API或字节码操作工具修改目标方法的字节码,以下是两种可行方案:
方案1:使用Unsafe类直接修改方法字节码
Unsafe是JDK内部API,可直接操作内存中的方法字节码,适合已加载类的方法修改:
// 在你的mixin方法末尾添加以下代码 // 获取Unsafe实例 Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); // 获取方法的methodData字段(HotSpot JVM结构) Field methodDataField = Method.class.getDeclaredField("methodData"); methodDataField.setAccessible(true); Object methodData = methodDataField.get(method); // 获取methodData中的code字段,替换为合并后的字节码 Field codeField = methodData.getClass().getDeclaredField("code"); codeField.setAccessible(true); unsafe.putObject(methodData, unsafe.objectFieldOffset(codeField), bytecode);
注意:Unsafe依赖JVM实现,不同厂商/版本兼容性差,生产环境需谨慎使用。
方案2:使用ASM修改类字节码并重定义类
通过ASM修改整个类的字节码,再借助Instrumentation重定义已加载类,兼容性更好:
步骤1:读取原类并创建ClassWriter
// 替换原代码中生成bytecode的部分 ClassReader cr = new ClassReader(method1Class.getName()); // 使用COMPUTE_FRAMES自动计算栈帧和局部变量表,避免手动计算的错误 ClassWriter classWriter = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
步骤2:自定义ClassVisitor替换目标方法
ClassVisitor cv = new ClassVisitor(Opcodes.ASM9, classWriter) { @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { // 匹配目标方法(方法名+描述符) if (name.equals(method.getName()) && desc.equals(descriptor)) { // 将合并后的方法指令写入ClassWriter mergedMethod.accept(super.visitMethod(access, name, desc, signature, exceptions)); return null; // 跳过原方法的处理 } return super.visitMethod(access, name, desc, signature, exceptions); } }; cr.accept(cv, ClassReader.SKIP_DEBUG); byte[] modifiedClassBytes = classWriter.toByteArray();
步骤3:重定义类(需Instrumentation实例)
如果能获取到Instrumentation对象,直接重定义类:
instrumentation.redefineClasses(new ClassDefinition(method1Class, modifiedClassBytes));
若没有Instrumentation,可通过自定义类加载器加载修改后的字节码,但仅对该加载器创建的类实例生效。
额外注意事项
- 直接拼接指令可能导致栈帧异常,建议使用
ClassWriter.COMPUTE_FRAMES让ASM自动计算栈帧和局部变量表,替代手动取maxStack/maxLocals最大值的逻辑。 - 注入逻辑需考虑方法的异常表、局部变量表冲突问题,避免合并后出现运行时错误。
- 若目标方法是静态方法、构造方法,需调整访问权限和指令处理逻辑,保证兼容性。
内容的提问来源于stack exchange,提问作者Qweru
相关产品推荐
相关产品推荐

