Skip to content
Jeyor1337 edited this page Oct 31, 2025 · 1 revision

YMixin 使用文档

YMixin 是一个轻量级的 Java 字节码插桩工具库,设计灵感来源于 Mixin(应该)。它旨在提供一种简单(并非简单)而强大的方式来修改 Java 类(其实更多是Minecraft)的行为,特别适用于需要热更新(Hot-Swap 人话:热注入)的场景。

目录

  1. 特性
  2. 安装
  3. 快速开始
  4. 核心 API
  5. 注解详解
  6. 处理代码混淆
  7. 构建
  8. 许可证

特性

  • 轻量级: 核心库小巧,依赖项少。
  • 热更新友好: 生成的字节码可直接用于 JVMTI 的热更新。
  • 运行时修改: 在程序运行时动态地修改类的字节码。
  • 注解驱动: 使用直观的注解来定义修改逻辑。
  • 支持混淆映射: 可以通过提供映射文件(如 SRG)来操作被混淆的代码。

安装

YMixin 使用 Maven 构建。要将 YMixin 添加到您的项目中,请去release页面下载jar文件。

快速开始

下面是一个简单的示例,演示如何使用 YMixin 向一个方法注入代码。

1. 目标类 (TargetClass.java)

// 这是我们想要修改的类
public class TargetClass {
    public void myMethod() {
        System.out.println("Hello, World!");
    }
}

2. Mixin 类 (MixinTarget.java)

import cn.yapeteam.ymixin.annotations.Inject;
import cn.yapeteam.ymixin.annotations.Mixin;
import cn.yapeteam.ymixin.annotations.Target;

// 使用 @Mixin 注解指定目标类
@Mixin(TargetClass.class)
public class MixinTarget {
    // 使用 @Inject 注解向目标方法注入代码
    @Inject(method = "myMethod", desc = "()V", target = @Target("HEAD"))
    public void beforeMyMethod() {
        // 这段代码将被注入到 myMethod 的开头
        System.out.println("Hello from Mixin!");
    }
}

3. 主程序 (Main.java)

import cn.yapeteam.ymixin.Transformer;
import cn.yapeteam.ymixin.YMixin;
import java.util.Objects;

public class Main {
    public static void main(String[] args) throws Throwable {
        // 1. 初始化 YMixin
        YMixin.init(
            // 提供一个 ClassProvider 用于根据类名加载类
            className -> {
                try {
                    return Class.forName(className);
                } catch (ClassNotFoundException e) {
                    return null;
                }
            },
            // 提供一个 ClassBytesProvider 用于根据类获取其字节码
            clazz -> {
                try {
                    return Main.class.getResourceAsStream("/" + clazz.getName().replace('.', '/') + ".class").readAllBytes();
                } catch (Exception e) {
                    return null;
                }
            }
        );

        // 2. 创建 Transformer 并添加 Mixin
        Transformer transformer = new Transformer();
        transformer.addMixin(MixinTarget.class);

        // 3. 执行转换
        byte[] modifiedBytes = transformer.transform().get("cn.yapeteam.ymixin.example.TargetClass");

        // 4. 加载并测试修改后的类
        // 注意:这里使用自定义 ClassLoader 来加载修改后的字节码
        CustomClassLoader loader = new CustomClassLoader();
        Class<?> modifiedClass = loader.defineClass(modifiedBytes);
        Object instance = modifiedClass.getDeclaredConstructor().newInstance();
        modifiedClass.getMethod("myMethod").invoke(instance);
    }
    
    // 一个简单的自定义 ClassLoader
    static class CustomClassLoader extends ClassLoader {
        public Class<?> defineClass(byte[] bytes) {
            return super.defineClass(null, bytes, 0, bytes.length);
        }
    }
}

预期输出:

Hello from Mixin!
Hello, World!

核心 API

1. 初始化 YMixin

在使用 YMixin 的任何功能之前,必须先调用 YMixin.init() 方法进行初始化。

public static void init(
    ClassProvider provider, 
    ClassBytesProvider bytesProvider, 
    @Nullable Logger logger, 
    @Nullable IMappingReader mappingReader, 
    @Nullable String mappingContent
);
参数 类型 描述
provider ClassProvider 一个函数式接口,用于根据类名字符串获取 Class<?> 对象。
bytesProvider ClassBytesProvider 一个函数式接口,用于根据 Class<?> 对象获取其原始字节码 byte[]
logger Logger (可选) 一个日志记录器接口,用于输出 YMixin 内部的操作信息。如果为 null,将使用默认的 System.out 记录器。
mappingReader IMappingReader (可选) 用于解析混淆映射内容的读取器。如果为 nullmappingContent 不为 null,则默认使用 SrgMappingReader
mappingContent String (可选) 包含代码混淆映射信息的字符串(例如,.srg 文件的内容)。

2. 使用 Transformer

Transformer 是执行字节码修改的核心类。

// 创建一个新的 Transformer 实例
Transformer transformer = new Transformer();

// 添加一个或多个 Mixin 类
// 可以通过 Class 对象、ClassNode 或字节码数组添加
transformer.addMixin(MyMixinClass1.class);
transformer.addMixin(myMixinClass2Bytes);

// 执行转换并获取结果
// 返回一个 Map,其中 key 是被修改类的完全限定名,value 是修改后的字节码
Map<String, byte[]> transformedClasses = transformer.transform();

// 获取特定类的修改后字节码
byte[] modifiedBytes = transformedClasses.get("com.example.TargetClass");

注解详解

@Mixin

  • 目标: 类
  • 作用: 指定当前类是一个 Mixin,并指向要修改的目标类。
@Mixin(TargetClass.class)
public class MyMixin {
    // ... Mixin 逻辑 ...
}

@Inject

  • 目标: 方法
  • 作用: 将注解所在方法的代码注入到目标类的指定方法中。
@Inject(
    method = "targetMethodName", // 目标方法的名称
    desc = "(Ljava/lang/String;)V", // 目标方法的描述符
    target = @Target(...) // 注入点
)
public void myInjection() {
    // 要注入的代码
}

@Overwrite

  • 目标: 方法
  • 作用: 完全重写(覆盖)目标类的某个方法。被注解的方法必须具有与目标方法兼容的签名。
@Overwrite(method = "methodToOverwrite", desc = "()I")
public int newMethodImplementation() {
    // 新的方法实现
    return 100;
}

@Shadow

  • 目标: 字段、方法
  • 作用: 在 Mixin 类中声明一个对目标类私有(或任意访问级别)字段或方法的引用,从而可以在 Mixin 代码中直接访问或调用它们。
@Mixin(TargetClass.class)
public class MyMixin {
    // 声明一个对 TargetClass 中 "private String secretField" 的引用
    @Shadow
    private String secretField;

    @Inject(method = "someMethod", desc = "()V", target = @Target("HEAD"))
    public void myInjection() {
        // 现在可以直接访问目标类的 secretField
        System.out.println("Accessing shadow field: " + this.secretField);
        this.secretField = "modified from mixin";
    }
}

@Local

  • 目标: 方法参数
  • 作用: 在 @Inject 方法中,用于捕获目标方法作用域内的局部变量或参数。
@Inject(method = "processData", desc = "(I)V", target = @Target("HEAD"))
public void beforeProcessData(
    // 捕获目标方法中索引为 1 的参数(通常是第一个参数,因为 0 是 this)
    @Local(source = "data", index = 1) int dataValue 
) {
    System.out.println("Injecting before processData, captured value: " + dataValue);
}
参数 类型 描述
source String 局部变量的名称(仅用于标识,实际映射靠 indextarget)。
target String (可选) 目标局部变量的名称。
index int (可选) 目标局部变量在局部变量表中的索引。

@Target

  • 目标: 注解参数
  • 作用: 作为 @Inject 的一部分,精确定义代码注入的位置。
@Target(
    value = "INVOKEVIRTUAL", // 目标操作码的名称 (来自 org.objectweb.asm.Opcodes)
    target = "java/io/PrintStream.println(Ljava/lang/String;)V", // 目标操作的描述
    shift = Target.Shift.BEFORE, // 注入时机:BEFORE 或 AFTER
    ordinal = 0 // 当有多个相同操作时,指定注入到第几个(从 0 开始)
)
  • value 可以是 HEAD(方法头)、RETURN(方法返回前),或任何 ASM Opcodes 中的指令名称。

@DontMap

  • 目标: 类、方法
  • 作用: 当启用了混淆映射时,阻止 YMixin 对被此注解标记的类或方法进行名称重映射。

处理代码混淆

YMixin 可以通过在 init 方法中提供映射文件内容来处理被混淆的代码。

  1. 读取映射文件: 将你的 .srg 或其他格式的映射文件读入一个 String
  2. 初始化 YMixin: 将该字符串传递给 init 方法的 mappingContent 参数。
String srgContent = Files.readString(Paths.get("mappings.srg"));

YMixin.init(
    classProvider,
    bytesProvider,
    null,
    null, // 使用默认的 SrgMappingReader
    srgContent
);
  • 如果你的映射文件不是 SRG 格式,你需要实现 IMappingReader 接口,并将其传递给 mappingReader 参数。

构建

该项目使用 Maven 进行构建。在项目根目录运行以下命令:

mvn -B package --file pom.xml

构建成功后,将在 target/ 目录下生成 ymixin.jar 文件。

许可证

YMixin 使用 GNU General Public License v3.0 许可证。详情请参阅 LICENSE 文件。(如果你是中国开发者你可以忽略,因为中国开发基本上不会管什么开源许可)