-
Notifications
You must be signed in to change notification settings - Fork 3
Home
Jeyor1337 edited this page Oct 31, 2025
·
1 revision
YMixin 是一个轻量级的 Java 字节码插桩工具库,设计灵感来源于 Mixin(应该)。它旨在提供一种简单(并非简单)而强大的方式来修改 Java 类(其实更多是Minecraft)的行为,特别适用于需要热更新(Hot-Swap 人话:热注入)的场景。
- 轻量级: 核心库小巧,依赖项少。
- 热更新友好: 生成的字节码可直接用于 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!
在使用 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 |
(可选) 用于解析混淆映射内容的读取器。如果为 null 但 mappingContent 不为 null,则默认使用 SrgMappingReader。 |
mappingContent |
String |
(可选) 包含代码混淆映射信息的字符串(例如,.srg 文件的内容)。 |
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(TargetClass.class)
public class MyMixin {
// ... Mixin 逻辑 ...
}- 目标: 方法
- 作用: 将注解所在方法的代码注入到目标类的指定方法中。
@Inject(
method = "targetMethodName", // 目标方法的名称
desc = "(Ljava/lang/String;)V", // 目标方法的描述符
target = @Target(...) // 注入点
)
public void myInjection() {
// 要注入的代码
}- 目标: 方法
- 作用: 完全重写(覆盖)目标类的某个方法。被注解的方法必须具有与目标方法兼容的签名。
@Overwrite(method = "methodToOverwrite", desc = "()I")
public int newMethodImplementation() {
// 新的方法实现
return 100;
}- 目标: 字段、方法
- 作用: 在 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";
}
}- 目标: 方法参数
-
作用: 在
@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 |
局部变量的名称(仅用于标识,实际映射靠 index 或 target)。 |
target |
String |
(可选) 目标局部变量的名称。 |
index |
int |
(可选) 目标局部变量在局部变量表中的索引。 |
- 目标: 注解参数
-
作用: 作为
@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(方法返回前),或任何 ASMOpcodes中的指令名称。
- 目标: 类、方法
- 作用: 当启用了混淆映射时,阻止 YMixin 对被此注解标记的类或方法进行名称重映射。
YMixin 可以通过在 init 方法中提供映射文件内容来处理被混淆的代码。
-
读取映射文件: 将你的
.srg或其他格式的映射文件读入一个String。 -
初始化 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 文件。(如果你是中国开发者你可以忽略,因为中国开发基本上不会管什么开源许可)