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);