理论存在

废话不多说,先看一段代码

import com.sun.tools.attach.VirtualMachine;

public class AttachExample {
    public static void main(String[] args) throws Exception {
        // 获取要附加到的 JVM 进程的进程 ID
        String pid = args[0];
        // 附加到该进程
        VirtualMachine vm = VirtualMachine.attach(pid);
        // 执行你想要的操作,例如:
        vm.loadAgent("my-agent.jar");
        // 不要忘记在最后断开连接
        vm.detach();
    }
}

众所周知,使用com.sun.tools.attach.VirtualMachine工具可以把一个agent注入到正在运行的JVM上加载

既然如此,那么是不是可以写一个agent,把这个agent注入到Minecraft客户端中,再让这个agent调用模组加载器(如Forge/Fabric)将我们想加载的mod加载进客户端呢?

public class MyInjectedCode {
    public static void agentmain(String args, Instrumentation inst) throws Exception {
        System.out.println("Hello from injected code!");
    }
    public static void premain(String args, Instrumentation inst) throws Exception 
    {
        System.out.println("Hello from injected code!");
    } 
}

这段代码在执行的时候会打印出"Hello from injected code!"字符串。

接下来把这段代码编译并打包成jar,注入进指定进程

import com.sun.tools.attach.VirtualMachine;

public class AttachExample {
    public static void main(String[] args) throws Exception {
        // 获取要附加到的 JVM 进程的进程 ID
        String pid = args[0];
        // 附加到该进程
        VirtualMachine vm = VirtualMachine.attach(pid);
        // 加载并注入代码(执行)
        vm.loadAgent("my-agent.jar", "MyInjectedCode");
        // 不要忘记在最后断开连接
        vm.detach();
    }
}

这样似乎就成功将一个agent注入进JVM并调用了。

实践开始

接下来进行实际测试

被注入的代码

首先,写一段输出字符串的代码,保存到MyInjectedCode.java文件中

java设置新模块启动 java启动器怎么加模组_java设置新模块启动

import java.lang.instrument.Instrumentation;
import java.io.*;

public class MyInjectedCode {
    public static void agentmain(String args, Instrumentation inst) throws Exception {
        System.out.println("(agentmain)Hello from injected code!");
        System.out.println("Args:" + args);
    }
    public static void premain(String args, Instrumentation inst) throws Exception 
    {
        System.out.println("(premain)Hello from injected code!");
        System.out.println("Args:" + args);
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clazz : classes) 
        {
           System.out.println(clazz.getName());
        }
    } 
}

编译这段代码,得到class文件

java设置新模块启动 java启动器怎么加模组_游戏_02


使用jar命令打包得到jar

java设置新模块启动 java启动器怎么加模组_java_03


打包命令:

jar --create --file "my-agent.jar" MyInjectedCode.class

用压缩软件打开jar文件,修改MANIFEST.MF文件,新增内容:

#建议去除上面Created-By这行
Agent-Class: MyInjectedCode
Premain-Class: MyInjectedCode
Can-Redine-Classes: true
Can-Retransform-Classes: true

java设置新模块启动 java启动器怎么加模组_jvm_04

注入工具

代码在下

import java.util.Properties;

import com.sun.tools.attach.VirtualMachine;

public class AttachExample {
    public static void main(String[] args) throws Exception {
        VirtualMachine vm = null; 
        String pid = args[0];
        vm = VirtualMachine.attach(pid);
        vm.loadAgent("C:/Users/xxxxxx/Desktop/测试/my-agent.jar", "MyInjectedCode");
        //注意!!!这里必须用完整的路径!!
        //否则抛异常Exception in thread "main" com.sun.tools.attach.AgentLoadException: Agent JAR not found or no Agent-Class attribute
        at jdk.attach/sun.tools.attach.HotSpotVirtualMachine.loadAgent(HotSpotVirtualMachine.java:163)
        //vm.executeAgent("MyInjectedCode");
        vm.detach();
    }
}

保存,编译完成之后,开始测试!

实际注入测试

这里拿MC来测试

先启动MC ( ̄︶ ̄*))

启动器里选择"测试游戏",方便查看日志输出

java设置新模块启动 java启动器怎么加模组_java设置新模块启动_05

然后等待一会,等到MC不输出日志了再操作(方便看qwq)

然后得获取到MC进程的PID才行

java设置新模块启动 java启动器怎么加模组_java_06

注意此时MC日志输出:

java设置新模块启动 java启动器怎么加模组_游戏_07


现在,启动注入工具!

java设置新模块启动 java启动器怎么加模组_游戏_08


额,报错了

先看看MC的日志:

java设置新模块启动 java启动器怎么加模组_java设置新模块启动_09


很显然,虽然我们的jar已经注入进去了,但是却没能成功加载

翻译一下,意思大概是这样:

类“MyInjectedCode”的签名者信息与同一包中其他类的签名者不匹配

此时不慌,先在MC里把Forge装上

java设置新模块启动 java启动器怎么加模组_游戏_10


和之前一样找到PID

java设置新模块启动 java启动器怎么加模组_java设置新模块启动_11


java设置新模块启动 java启动器怎么加模组_jvm_12


成功被执行啦 φ(゜▽゜*)♪ !

(测试版本:1.19.3)

【但是谁会去往原版端里注入模组啊喂( ̄▽ ̄)"】

关于原版端注入的尝试

从上文得知,貌似因为Mojang给Minecraft Jar上了签名,导致咱们的Jar包签名和Mojang的不同引发异常

【获取到PID,图和步骤略】

java设置新模块启动 java启动器怎么加模组_jvm_13


java设置新模块启动 java启动器怎么加模组_jvm_14


那么现在,删除原版端的签名试一试吧!

首先,找到Jar文件!

java设置新模块启动 java启动器怎么加模组_java设置新模块启动_15


(作者这里开了启动器的版本隔离)

用压缩软件打开这个jar文件,进入META-INF文件夹

删除名为所有后缀名为"RSA",“SF”,"DSA"的文件

java设置新模块启动 java启动器怎么加模组_游戏_16


编辑"MANIFEST.MF"文件

只保留最前面这两行,其他全删掉:

Manifest-Version: 1.0
Main-Class: net.minecraft.client.Main

但是!实际测试还是出现和上文相同的情况!

后来发现,在目录".minecraft\libraries\net\minecraft\client"中还有一些Jar文件,作者这里就不对其进行去签名注入测试了,感兴趣的小伙伴可以自己尝试一下

关于Fabric/Quilt/LiteLoader端的尝试

和上文一样,安装对应的加载器并运行注入工具进行测试

图片上传不了了,直接看结果吧!

Fabric端注入成功!
(测试版本:1.19.3)

java设置新模块启动 java启动器怎么加模组_java_17


Quilt Loader端注入成功!

(测试版本:1.19.3)

java设置新模块启动 java启动器怎么加模组_jar_18


原版 + OptiFine 注入成功!

(测试版本:1.18.2)

java设置新模块启动 java启动器怎么加模组_java_19


LiteLoader 注入遇到问题

java设置新模块启动 java启动器怎么加模组_游戏_20


(测试版本:1.12.2)

这里是因为我启动Minecraft 1.12.2用的是Java11,而我编译Agent时用的是JDK18,所以到了这里就寄了

该问题由Java版本所致,解决起来很简单,这里作者就不做过多的测试了,感兴趣的小伙伴可以自己测试哦╰(°▽°)╯

注入模组的思路和这种方法的缺点

上文只是把代码注入进了游戏里,那么怎么让这段代码加载模组呢?

作者的思路是调用模组加载器提供的API等接口,让游戏加载

但是这种方法有一个缺点,即Mixin可能无法使用

Mixin为模组开发者提供了一种更方便地修改Minecraft源码的方式,使用Mixin的模组应该需要在Minecraft启动前由模组加载器加载到游戏中,现在直接把模组注入进已经加载好的Minecraft中,Mixin当然也就歇菜了