如何在已经运行的jvm之中应急调用临时代码?

52 阅读2分钟
  1. 使用arthas,通过jad、mc、retransformer可以将一个类中的某个方法进行改造,变成需要的方法,并且可以通过vmtool进行调用 这里可以通过arthas的官方文档操作,对当前类有侵入。 arthas.aliyun.com/doc/retrans…

  2. 不使用arthas,而是用自己的java代码该如何实现呢? 实际上arthas也是使用了JVMTI这一类工具(大概是这个名吧) 我们自己也可以编写这样的工具,在测试环境里面随便玩,生产环境遇到问题应急也可以用用(大概 一步一步编写,先通过常规手段来进行类加载。

(1)需要自己写一个classloader,可以加载自己的类,这个类需要在当前运行的jvm之中

package test;

/**
 * 为了多次载入执行类而加入的加载器
 * 把defineClass方法开放出来,只有外部显式调用的时候才会使用到loadByte方法
 * 由虚拟机调用时,仍然按照原有的双亲委派规则使用loadClass方法进行类加载
 *
 * @author zzm
 */
public class HotSwapClassLoader extends ClassLoader {

    public HotSwapClassLoader() {
        super(HotSwapClassLoader.class.getClassLoader());
    }

    public Class loadByte(byte[] classByte) {
        return defineClass(null, classByte, 0, classByte.length);
    }

}

(2)通过编写agent代码来创建agent jar,通过这个jar可以attach到对应的jvm之中

package agent;

import java.lang.instrument.Instrumentation;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class MyAgent {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        try {
            System.out.println(agentArgs);
            String[] args = agentArgs.split(",");
            String loadClassPath = args[0];
            String className = args[1];
            String methodName = args[2];
            // 加载你的类
            Path path = Paths.get(loadClassPath);
            byte[] bytes = Files.readAllBytes(path);
            Class<?> myClass = Class.forName("test.HotSwapClassLoader");
            Object hotSwapLoader = myClass.newInstance();
            myClass.getMethod("loadByte", byte[].class)
            .invoke(hotSwapLoader, new Object[]{bytes});
            // 调用方法
            Class instanceCls = Class.forName(className);
            Object obj = instanceCls.newInstance();
            instanceCls.getMethod(methodName).invoke(obj);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

(3)编写对应的attach jar包,包括你想要添加的class,然后是想要反射执行的方法

package test;

import com.sun.tools.attach.VirtualMachine;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.StringJoiner;

public class AgentAttacher {
    public static void main(String[] args) throws Exception {
        String jvmPid = "12345"; // JVM的PID
        String agentJar = "java-agent-1.0-SNAPSHOT.jar";

        String[] agentArgs = new String[3];

        String classPath = "Retransform2.class";
        byte[] bytes = Files.readAllBytes(Paths.get(classPath));
        agentArgs[0] = classPath;
        agentArgs[1] = "test.Retransform2";
        agentArgs[2] = "testMyMethod";
        StringJoiner joiner = new StringJoiner(",");
        for (String arg : agentArgs) {
            joiner.add(arg);
        }
        System.out.println(joiner.toString());
        VirtualMachine vm = VirtualMachine.attach(jvmPid);
        vm.loadAgent(agentJar, joiner.toString());
        vm.detach();
    }
}

(4)添加自己喜欢的功能吧,比如让输出可以打印在某个文件之中。最关键的依然是classloader这块。

  1. 不要通过内置的classloader,而是通过agent来植入classloader

直接把上面的hotSwapClassLoader挪到agent代码中,直接在agent调用就可以植入了

因为很多远程工具都是不需要对原生代码做任何改动的,那么可以认为它们实际上是有着自己的classloader的,这些classloader会将必要的类attach到对应的jvm之中。

实际上可以说,attach模式可以执行任何java代码。

  1. 可重复加载的类

在agent末尾处对类进行回收,通过置null以及System.gc可以让新的类重新加载。