在不重新编译的情况下直接修改 Java Class 文件中的内容

Java 程序实际上执行的是 Java 文件编译后的 Class 文件,这是任何一个 Java 开发人员都了解的基本知识。若 Java 程序执行的结果不符合要求,通常的解决方法是先修改 Java 文件,重新编译成 Class 文件后再次执行。但有时候我们不能直接修改 Java 文件(如只有包含 class 文件的 jar 包),此时我们就只能直接修改 Class 文件,本文将展示在基于不同的需求通过可视化工具和 Javassist 库来直接对 Class 文件进行修改的方法。

注:由于直接修改 class 文件会涉及到 class 文件结构的相关知识,所以利用此种方式时最好对 class 文件结构有一定的了解

修改 Class 文件中的变量

下面的代码为一个典型的输出 Hello World 的 Java 小程序

package com.lucumt;
public class Test {
    public static String language = "Java";
    public static void main(String[] args) {
        sayHello();
    }
    public static void sayHello() {
        System.out.println("=====Hello "+language+" World!======");
    }
}

在 cmd 命令行中运行该程序的结果如下

若想将运行结果从 Hello Java World 修改为 Hello Golang China ,除了通过修改源代码重新编译运行这个方法之外我们还可以利用工具直接修改原有的 class 文件来实现。

首先从 JBE 官网** 下载 JBE(Java Bytecode Editor),JBE 是一个用于浏览和修改 Java Class 文件的开源软件,在其官网上可以看到如下图所示的说明信息

备份下载地址:jbe-0.1.1.zip

下载完该软件后,在该软件中打开我们要修改的 Class 文件

首先我们需要将静态变量 language 的值从 Java 修改为 Golang , 由于 language 是一个静态变量,故我们需要在 class 文件的 clinit 方法中找到该变量并修改其值。如下图所示,展开 clinit 并切换到 Code Editor 页,可以看到 language 的值为 Java ,在 Code Editor 部分将 Java 修改为 Golang 然后点击 Save method 即可完成静态变量值的修改。

接着展开 sayHello 方法,同样切换到 Code Editor 页,将 World 修改为 China 后点击 Save method,至此整个修改操作完成。

在命令行中重新执行该程序,输出结果为 Hello Golang China ,符合我们的要求。

修改 Class 文件中的方法

对于较为简单的修改需求我们可以利用 JBE 等工具来直接修改,若要对 class 文件进行较为复杂的修改,如增加新方法,修改已有方法的实现逻辑等,对于此种需求虽然也可以用 JBE 实现目的,但工作量很大,容易出错,此时 JBE 已经不太适合使用,需要寻找其它更快捷的方法。

由于 Java 文件后生成的 class 文件是一个包含 Java 字节码的二进制文件,程序最终执行的就是二进制文件中的字节码,我们的需求可以归纳为如何修改 Java 字节码文件。前一部分通过 JBE 来修改 class 文件只不过是将这个过程进行了图形化封装,我们需要找到更底层的实现方法来适应我们的需求。

此时 Javassist* 闪亮登场!在 Javassit 官网关于其的第一句介绍为 Javassist (Java Programming Assistant) makes Java bytecode manipulation simple. It is a class library for editing bytecodes in Java* 。Javassist 天生就是为修改 Java 字节码而来的,它提供了源代码和字节码两种级别的 API 接口,为了实现的简便性,本文主要介绍利用源代码 API 来修改 class 文件。

下面的代码为一个计算两个整数相加的程序

package com.lucumt;
public class Test1 {
    public static void main(String[] args) {
          Test1 t1 = new Test1();
          int result = t1.addNumber(3, 5);
          System.out.println("result is: "+result);
    }

    public int addNumber(int a,int b){
        return a+b;
    }
}

正常情况下,其输出结果如下

若我们想将 addNumber 的返回结果从两个数之和变为两个数立方后求和,则可以利用 Javassist 提供的 API 通过 Java 程序来直接修改 class 文件。

关于如何使用 Javassist,请直接参看相应的 入门教程* ,本文不再详细说明,利用 Javassist 修改 addNumber* 的 Java 代码如下:

package com.lucumt.test;
import java.io.IOException;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
public class UpdateMethod {
    public static void main(String[] args) {
        updateMethod();
    }

    public static void updateMethod(){
        try {
            ClassPool cPool = new ClassPool(true);
                //如果该文件引入了其它类,需要利用类似如下方式声明
            //cPool.importPackage("java.util.List");
    
            //设置class文件的位置
            cPool.insertClassPath("D:\\Java\\eclipse\\newworkspace\\test\\bin");
    
            //获取该class对象
            CtClass cClass = cPool.get("com.lucumt.Test1");
    
            //获取到对应的方法
            CtMethod cMethod = cClass.getDeclaredMethod("addNumber");
    
            //更改该方法的内部实现
            //需要注意的是对于参数的引用要以$开始,不能直接输入参数名称
            cMethod.setBody("{ return $1*$1*$1+$2*$2*$2; }");
    
            //替换原有的文件
            cClass.writeFile("D:\\Java\\eclipse\\newworkspace\\test\\bin");
    
            System.out.println("=======修改方法完=========");
        } catch (NotFoundException e) {
            e.printStackTrace();
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行该代码后重新执行 Test1 后的结果如下,从图中可以看出运行结果符合预期

关于 UpdateMethod 工具类有如下几点说明:

  • 如果要修改的 class 文件中引入了其它类,需要调用 ClassPool 中的 importPackage 方法引入该类,否则程序会报错
  • 修改完后,一定要调用 CtClass 中的 writeFile 方法覆盖原有的 class 文件,否则修改不生效
  • 在修改方法的过程中若要引用方法参数,不能在修改程序代码中直接写该参数,否则程序会抛出 javassist.CannotCompileException: [source error] no such field: 异常。在本例中 addNumber 的两个参数分别为 ab ,在修改时不能写成 cMethod.setBody("{ return a*a*a+b*b*b; }") 需要修改为 cMethod.setBody("{ return $1*$1*$1+$2*$2*$2; }")
  • 在 Javassist 的 Introspection and customization** 部分有如下一段话
    The parameters passed to the target method are accessible with $1, $2, … instead of the original parameter names. $1 represents the first parameter, $2 represents the second parameter, and so on. The types of those variables are identical to the parameter types. $0 is equivalent to this. If the method is static, $0 is not available.
    从中可知,方法中的参数从 $1* 开始,若该方法为非 *static* 方法,可以用 *$0 来表示该方法实例自身,若该方法为 static 方法,则 $0 不可用

在 Class 文件中增加方法

Javassist 不仅可以修改已有的方法,还可以给 class 文件增加新的方法。仍以前面的 Test1 Java 代码中为例,现要求增加一个名为 showParameter 的方法并在 addNumber 方法中调用,其主要功能是输出 addNumber 中传入的参数。利用 Javassist 修改 class 文件实现该功能的代码如下

package com.lucumt.test;
import java.io.IOException;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.NotFoundException;
public class AddMethod {
    public static void main(String[] args) {
        addMethod();
    }

    public static void addMethod(){
        try {
            ClassPool cPool = new ClassPool(true);
            cPool.insertClassPath("D:\\Java\\eclipse\\newworkspace\\test\\bin");
            CtClass cClass = cPool.get("com.lucumt.Test1");
    
    
            CtMethod cMethod = cClass.getDeclaredMethod("addNumber");
    
            //增加一个新方法
            String methodStr ="public void showParameters(int a,int b){" 
                        +"  System.out.println(\"First parameter: \"+a);"
                        +"  System.out.println(\"Second parameter: \"+b);"
                        +"}";
            CtMethod newMethod = CtNewMethod.make(methodStr, cClass);
            cClass.addMethod(newMethod);
    
            //调用新增的方法
            cMethod.setBody("{ showParameters($1,$2);return $1*$1*$1+$2*$2*$2; }");
            cClass.writeFile("D:\\Java\\eclipse\\newworkspace\\test\\bin");
    
        } catch (NotFoundException e) {
            e.printStackTrace();
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行该代码后重新执行 Test1 后的结果如下,从图中可以看出运行结果符合预期

从上述代码可以看出,利用 Javassist 增加方法比修改方法更简单,先将要新增的方法内容赋值到字符串,然后分别调用相关类的 makeaddMethod 方法即可。

后记

利用 JBE 或 Javassist 虽然可以实现直接修改 class 文件的内容,但毕竟属于不正规的做法,可能会导致后续版本不一致等问题,在条件允许的情况下还是要尽量通过修改 Java 文件然后重新编译的方式来实现目的。

posted @ 2023-03-14 13:20:00 猎隼丶止戈 阅读(39) 评论(0)
发表评论
昵称
邮箱
网址