如何动态修改JVM的字节码

一.概述

最近在做一个需求的时候,需要在JVM启动好之后,能够动态的修改JVM已经加载的一个类的一个方法,把这个方法的返回值直接改成返回true。

上述需求概括为动态修改JVM字节码,我们需要借助修改字节码的工具,同时也要让启动中的JVM能感知到我们的修改,这个需要借助java的instrument。下面我们就来看一下具体的实现。

二.实现

1.编写修改字节码的agent

在JVM已经加载的类中找到要修改的类,然后使用javassist从磁盘上读取到要修改类的字节码,修改指定方法的返回值后,让JVM重新再加载一下我们刚才修改的这个类,具体代码如下:
agent的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class XxxmockAgent {
    // 指定我们要修改字节码的类的全限定名
    private static final String CLASS_NAME = "xxxCommonBO";
    @SuppressWarnings("rawtypes")
    public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException{
        System.out.println("loadagent after main...");
        //获取当前JVM已经加载过的所有类
        Class[] classes =  inst.getAllLoadedClasses();
        for (Class clazz : classes) {
	    //找到需要修改的类
            if(clazz.getName().equals(CLASS_NAME)) {
                System.out.println("find class " + CLASS_NAME);
                //按照要求字节吗
                inst.addTransformer(new XxxCommonTransformer(), true);
                //让JVM重新加载修改过字节码的类
                inst.retransformClasses(clazz);
            }
        }
        System.out.println("loadagent after main sucess...");
    }
}

修改字节码的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class XxxCommonTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {
        String compareClass = className.replace('/', '.');
        System.out.println("transformer..." + compareClass);
        try {
	    //构建javassist需要ClassPool
            ClassPool classPool = ClassPool.getDefault();
            //把要修改的类的classpath加入到javassist的ClassPool中
            classPool.appendClassPath("/xxx/WEB-INF/lib/*");
            //从磁盘上读取要修改类的字节码,并且转换成javassit中的CtClass模型
            CtClass ctClass = classPool.get(compareClass);
            //获取需要修改的字节码的方法
            CtMethod ctMethod = ctClass.getDeclaredMethod("isFromA");
            //修改方法体
            ctMethod.setBody("return true;");
            //写入修改后的字节码
            ctClass.writeFile();
            return ctClass.toBytecode();
        } catch (Exception e) {
	    e.printStackTrace();
        }
        return null;
    }
}
2.编写attach到JVM的client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Client {
    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println("[usage:java -jar client-1.0.0.jar pid path] and args.lenght="+args.length);
            return;
        }
        // 第0个参数是要attach的JVM进程ID
        String pid = args[0];
        // 第1个参数是agent JAR包所在的路径
        String agentPath =args[1];
        System.out.println("pid:" + pid);
        System.out.println("agentPath:" + agentPath);
        try {
            attach(pid, agentPath);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public static void attach(String pid, String agentPath) throws Exception {
        try {            
            //attach到远程JVM上去
            VirtualMachine  vm = VirtualMachine.attach(pid);
            //加载agent
            vm.loadAgent(agentPath);
        } catch (RuntimeException re) {
            throw re;
        } catch (IOException ioexp) {
            throw ioexp;
        } catch (Exception exp) {
            exp.printStackTrace();
            throw exp;
        }
    }
}

三.注意点

1.在编写agent的时候我们需要指定要修改的类所在的classpath,此时的类加载器是AppClassLoader,如果你要attach的JVM进程是用jetty&&tomcat等容器启动起来的,必须要指定要修改的类所在的classpath。
2.在agent中我们使用到javassit这个开源的操作字节码的二方库,加载javassit中类的类加载器也是AppClassLoader,同样如果你的JVM进程是用jetty或者tomcat启动的话,而且你的应用中已经包含了javassit这个二方库,AppClassLoader也加载不了,会出现ClassPool加载失败的异常,此时需要我们显式地把javassit包含到agent中去,如果你的agent是使用maven构建的话,你可以使用maven-shade-plugin这个maven插件,该插件既能把依赖的jar聚合起来,也能在jar包中自动生成MANIFEST.MF。
3.在编写client的时候我们用到了VirtualMachine这个类,这个类在tools.jar中,这个jar是jdk自己携带的。如果你是用maven来构建你的client的话,你可以用下面方式把tools.jar自动引入到你的project中。

1
2
3
4
5
6
7
<dependency>					
	<groupId>com.sun</groupId>
	<artifactId>tools</artifactId>
	<version>1.6</version>
	<scope>system</scope>
	<systemPath>${JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>

4.在运行client的方式,我们通过java -jar来运行,此时需要tools.jar,如果你不想把tools.jar打包的client中,你需要在运行client JAR的时候带上tools.jar.

1
java -Xbootclasspath/a:tools.jar  -jar /home/xxx/client-1.0.0.jar 22410 /home/xxx/agent-1.0.0.jar

22410是进程PID