1.背景
在某些情况下,无法获取或修改源码情况,我们需要直接修改class字节码文件,以解决项目中运行存在的问题。在网上查询一番后,尝试过使用Jclasslib,虽然也很强大,但是对于使用者不是很友好,一方面是直接和字节码打交道,另一方面是只能处理变量值修改等简单的情况。
相比之下,javassist就友好很多,用代码的形式去修改class内容,并且功能也基本满足我们的修改需求。
2.javassist中文文档
如果需要深入学习的可以直接在网上找到相应的中文文档,方便学习摸索。
3.软件准备
对于反编译的软件,大家都会用到jd-gui,具体使用这里不再赘述。也可以直接将class拉到idea查看也很方便。
4.修改步骤
(1)引入依赖

<dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.25.0-GA</version>
        </dependency>

(2)修改示例
存在一个TestClass类,反编译class后得到一下内容

package com.company.test;

public class TestClass {
    private int id;
    private String name = "zhangsan";
    private int age;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public TestClass(int id,String name,int age){
        this.id = id;
        this.name = name;
        this.age = age;
    }
}

以下是修改class的测试类,可满足简单的修改需求,复杂的需求可以查看官方文档学习使用。

package com.company.test;

import javassist.*;

public class Test {

    public static void main(String[] args) throws Exception {
        // 加载Class池
        ClassPool pool = ClassPool.getDefault();
        // 添加Class目录,即jar包解压根目录
        pool.appendClassPath("C:\\Users\\Administrator\\Desktop\\test");
        // 或直接适用jar包路径
        //pool.insertClassPath("C:\\Users\\Administrator\\Desktop\\test\\xx.jar");
        // 获取已存在的Class对象,填写包全路径和类名
        CtClass testJarClass = pool.getCtClass("com.company.test.TestClass");

        //addConstructor(pool,testJarClass);
        //addMemberVar(testJarClass);
        //addMethod(pool,testJarClass);
        insertCode(pool,testJarClass);

        // 填写生成目录,写出到新文件
        testJarClass.writeFile("C:\\Users\\Administrator\\Desktop\\test\\target");

    }

    /**
     * @descriptoin: 增加成员变量
     * @param testJarClass: 加载需要修改的class
     */
    private static void addMemberVar(CtClass testJarClass) throws Exception {
        // 直接添加想要的代码内容
        CtField f = CtField.make("public int z = 0;", testJarClass);
        testJarClass.addField(f);
    }

    /**
      * @descriptoin: 增加方法,也可用于某方法内局部变量赋值思路
      * @param pool: class池
      * @param testJarClass: 加载需要修改的class
    */
    private static void addMethod(ClassPool pool,CtClass testJarClass) throws Exception {
        // 设置方法内容
        String methodStr = "public String getCustomerName() {\n" +
                "   return \"wangwu\"; \n" +
                "}";
        CtMethod m = CtNewMethod.make(methodStr,testJarClass);

        // 设置异常
        m.setExceptionTypes(new CtClass[]{pool.get("java.lang.Exception")});
        testJarClass.addMethod(m);
    }

    /**
      * @descriptoin: 在方法内插入代码
      * @param pool: class池
      * @param testJarClass: 加载需要修改的class
    */
    private static void insertCode(ClassPool pool,CtClass testJarClass) throws Exception {
        CtMethod m = testJarClass.getDeclaredMethod("setName");
        String code = "System.out.println(\"this is name:\"+$1);";
        // 在方法开头插入代码
        //m.insertBefore(code);
        // 在方法结尾插入代码,适用于方法无返回值情况
        //m.insertAfter(code);
        // 在方法指定行数插入代码,若该行存在代码,则在开头插入
        //m.insertAt(17,code);
        // 重新设置方法体
        m.setBody(code);
    }

    /**
      * @descriptoin: 添加有参构造函数
      * @param pool: class池
      * @param testJarClass: 加载需要修改的class
    */
    private static void addConstructor(ClassPool pool,CtClass testJarClass) throws Exception{
        /*
          * 创建有参的构造函数
          * pool.get()字符串参数填写实际方法的参数类型,填写包全路径,基本类型直接填写类型
          * testJarClass 表示需要修改已加载的class
        */
        CtConstructor cons = new CtConstructor(new CtClass[]{pool.get("int"),pool.get("java.lang.String"),pool.get("int")}, testJarClass);

        // 填写方法体
        // $0=this / $1,$2,$3... 代表方法参数
        cons.setBody("{\n" +
                "        $0.id = $1;\n" +
                "        $0.name = $2;\n" +
                "        $0.age = $3;\n" +
                "    }");
    }
}

5.注意事项
(1)设置方法体使用参数时,$0表示this,$1表示参数1,$2表示参数2以此类推
(2)设置方法体引用外部类时候,需要填写全路径类,否则可能报xxx不存在。例如:

String str = xxx.xxx.xxx.StrFun.getString();

其中StrFun可能是其他模块的类,也需要将该模块包反编译到相同根目录下。
(3)装箱和拆箱操作是语法糖。对于字节码说来,是不存在装箱和拆箱的。所以Javassist的编译器不支持装箱拆箱操作:

Integer i = 3;

可以看出,此装箱操作是隐式的。但是在Javassist中,你必须显式的将值类型从int转为Integer:

Integer i = new Integer(3);