【问题标题】:Modifying jarfile at runtime在运行时修改 jarfile
【发布时间】:2021-05-19 21:21:28
【问题描述】:

我有一个关于 java 的问题。是否可以在 java 程序中加载、获取类和该类中的方法、修改方法、重新编译 jarfile 并覆盖它?

示例: a.jar 有一个类,使用静态 void main() 方法,只打印“world”

我的目标是加载 a.jar 的主类,获取静态 void main() 方法,并在顶部添加另一个打印“hello”的调用,然后再次将其保存到磁盘。所以现在如果我运行 a.jar,它会说“world”,然后我运行我的程序,它会保存 jarfile,然后我再次运行 a.jar,它会说“hello”,然后是“world”。

这样的事情可能吗?如果可以,怎么做?

【问题讨论】:

  • 这对于无需更改 Java 代码和 JAR 内容即可解决的问题似乎相当复杂

标签: java


【解决方案1】:

您可以使用 Java 的 ASM 库来执行此操作。我自己对此进行了测试,效果很好。

对于较新版本的Java > 8,您必须将export jdk.internal.org.objectweb.asm 添加到当前模块。或者如果您不想使用内部类,您可以下载objectweb.asm,将其添加为依赖项并使用它与下面的方式完全相同..

代码:

import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
import jdk.internal.org.objectweb.asm.tree.ClassNode;
import jdk.internal.org.objectweb.asm.tree.FieldNode;
import jdk.internal.org.objectweb.asm.util.CheckClassAdapter;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;


public class Main {
    public static void main(String[] args) {
        Main cls = new Main();

        //Add a new field to the class called "Main".
        //This field will be called "key" and will have a value of "SomeValue".

        if (cls.getSelfValue("key") == null) {
            System.out.println(cls.modifySelf("key", "SomeValue")); //Prints true upon success.
            System.out.println(cls.getSelfValue("key")); //Prints "SomeValue".
        }
        else {
            System.out.println("Key field already exists with a value of: " + cls.getSelfValue("key"));
        }
    }

    //Load the byte-code for the current class.
    private ClassNode getSelf() {
        try {
            ClassNode node = new ClassNode();
            ClassReader reader = new ClassReader(this.getClass().getSimpleName());
            reader.accept(node, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
            return node;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    //Save the byte-code for the current class.
    private boolean setSelf(ClassNode node) {
        try {
            //Create a class writer. use a ClassCheckAdapter on it to make sure we aren't making any mistakes! The adapter checks whether or not our byte-code makes sense.
            ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
            node.accept(new CheckClassAdapter(writer, false));

            //Save the byte-code for the current class.
            DataOutputStream stream = new DataOutputStream(new FileOutputStream(new File(ClassLoader.getSystemResource(this.getClass().getSimpleName().replace('.', '/') + ".class").getPath())));
            stream.write(writer.toByteArray());
            stream.flush();
            stream.close();
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    //Add a new field of type "String" to the current class. Give it a name and a value.
    private boolean modifySelf(String name, String value) {
        ClassNode node = this.getSelf(); //Load the byte-code for the current class.
        node.fields.add(new FieldNode(Opcodes.ASM4, name, "Ljava/lang/String;", null, value)); //Add a new field to the class.
        return setSelf(node);
    }

    //Get the field with the specified name from the current class. Return its value.
    private String getSelfValue(String name) {
        ClassNode node = this.getSelf(); //Load the byte-code for the current class.
        for (FieldNode f : node.fields) {
            if (f.name.equals(name) && f.desc.equals("Ljava/lang/String;")) {
                return (String)f.value;
            }
        }
        return null;
    }
}

上面的代码添加了一个字段。对modifymain方法,你可以这样做:

for (MethodNode method : mainClassNode.methods) {
    if (method.name.equals("main") && method.desc.equals("()V")) {
        method.instructions.clear();

        method.instructions.add(new LdcInsnNode("Hello"));
        method.instructions.add(new VarInsnNode(Opcodes.ASTORE, 0));
        method.instructions.add(new TypeInsnNode(Opcodes.NEW, "java/lang/StringBuilder"));
        method.instructions.add(new InsnNode(Opcodes.DUP));
        method.instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false));
        method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0));
        method.instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false));
        method.instructions.add(new LdcInsnNode(" - World"));
        method.instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false));
        method.instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false));

        method.instructions.add(new VarInsnNode(Opcodes.ASTORE, 0));

        //Add more instructions to actually call `System.out.println` on `ALOAD_0` (aka stack variable 0.. aka our `StringBuilder.toString()`)
    }
}

【讨论】:

  • 请注意,此解决方案使用 JDK 的内部类,这些类可能会在没有警告的情况下进行更改,因此这可能不适用于未来的 Java 版本。您不应该使用 JDK 的内部类。
  • 您可以通过使用instrumentation 定义ClassFileTransformer 并将ASM 作为单独的依赖项导入ASM,以更加面向未来的方式使用它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-03-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多