Javassist
Javassist (JAVA programming ASSISTant) 是在 Java 中编辑字节码的类库;它使 Java 程序能够在运行时定义一个新类, 并在 JVM 加载时修改类文件。原理与反射类似,但开销相对较低。
常用API
ClassPool
-
getDefault : 返回默认的 ClassPool 是单例模式的,一般通过该方法创建我们的 ClassPool;
-
appendClassPath, insertClassPath : 将一个 ClassPath 加到类搜索路径的末尾位置 或 插 入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中 找不到类的尴尬;
-
get , getCtClass : 根据类路径名获取该类的 CtClass 对象,用于后续的编辑。
-
makeClass:创建一个新的类。
CtClass
-
freeze : 冻结一个类,使其不可修改;
-
isFrozen : 判断一个类是否已被冻结;
-
defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被 defrost, 则禁止 调用 prune 方法;
-
prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无 法正常使用,慎用;
-
detach : 将该 class 从 ClassPool 中删除;
-
writeFile : 根据 CtClass 生成 .class 文件;
-
toClass : 通过类加载器加载该 CtClass。
CtMethod
-
insertBefore : 在方法的起始位置插入代码;
-
insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到 exception;
-
insertAt : 在指定的位置插入代码;
-
setBody : 将方法的内容设置为要写入的代码,当方法被 abstract 修饰时,该修饰符被 移除;
-
make : 创建一个新的方法。
Javaassist 操作字节码示例
1、创建Hello类
public class Demo1 {
public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException {
ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.makeClass("Javassist.Hello");
ctClass.writeFile();
}
}
2、添加属性
public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException {
//1、创建Hello类
ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.makeClass("Javassist.Hello");
//2、添加属性
CtField name = new CtField(cp.get("java.lang.String"), "name", ctClass);
name.setModifiers(Modifier.PUBLIC);
ctClass.addField(name,CtField.Initializer.constant("Sentiment"));
ctClass.writeFile();
}
属性赋值时也可用:
ctClass.addField(name,"name=\"Sentiment\"");
但这种赋值偏向于用构造器等进行初始化
3、添加方法
可以设置的返回类型:
public static CtClass booleanType;
public static CtClass charType;
public static CtClass byteType;
public static CtClass shortType;
public static CtClass intType;
public static CtClass longType;
public static CtClass floatType;
public static CtClass doubleType;
public static CtClass voidType;
这里可以发现不支持String类型,在Java字节码中,String类型在方法的参数列表和返回值类型中,通常不是直接使用字符串,而是使用字符串在常量池中的索引值。如果想设置String类型的话可以用:cp.getCtClass("java.lang.String")
CtMethod ctMethod = new CtMethod(CtClass.voidType, "Hello1", new CtClass[]{CtClass.intType, CtClass.charType}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctClass.addMethod(ctMethod);
ctClass.writeFile();
设置方法体
ctMethod.setBody("System.out.println(\"This is test !\");");
在方法体的前后分别插入代码
这里有参构造的形参是var1,如果要输出var1,就要用到特殊变量$1、$2(具体使用后边再说)
CtMethod ctMethod = new CtMethod(CtClass.voidType, "Hello1", new CtClass[]{CtClass.intType, CtClass.charType}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("System.out.println(\"This is test !\");");
ctClass.addMethod(ctMethod);
ctMethod.insertBefore("System.out.println(\"我在前面插入:\"+$1);");
ctMethod.insertAfter("System.out.println(\"我在后面插入了:\"+$2);");
ctClass.writeFile();
4、添加构造器
直接添加的有参构造,无参构造去掉中间的参数即可
CtConstructor cons = new CtConstructor(new CtClass[]{cp.getCtClass("java.lang.String")}, ctClass);
cons.setBody("{name=\"Sentiment\";}");
ctClass.addConstructor(cons);
设置name=var1
{$0.name = $1;}
5、修改已有类
可以通过ClassPool的get方法获取已有类,并进行修改
package Javassist;
import javassist.*;
import java.io.IOException;
public class Demo02 {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.get("Javassist.Test");
CtConstructor test = ctClass.getConstructors()[0];
test.setBody("{System.out.println(\"Changing......\");}");
ctClass.writeFile();
}
}
class Test {
public static String name = "Sentiment";
public Test() {
System.out.println("This is test !");
}
}
6、加载字节码
通过自带的toBytecode()转换下即可
ctClass.toBytecode();
ctClass.toClass().newInstance();
Javassist 特殊变量
标识符 | 作用 |
---|---|
$0、$1、$2、 3 、 3、 3、… | this和方法参数(1-N是方法参数的顺序) |
$args | 方法参数数组,类型为Object[] |
$$ | 所有方法参数,例如:m($$)相当于m($1,$2,…) |
$cflow(…) | control flow 变量 |
$r | 返回结果的类型,在强制转换表达式中使用。 |
$w | 包装器类型,在强制转换表达式中使用。 |
$_ | 方法的返回值 |
$sig | 类型为java.lang.Class的参数类型对象数组 |
$type | 类型为java.lang.Class的返回值类型 |
$class | 类型为java.lang.Class的正在修改的类 |
1、$0,$1,$2,…
$0代表this,$1、$2代表方法的形参,通过上边例子也不难看出。这里需要注意:静态方法是没有$0的
2、$args
$args变量表示所有参数的数组,它是一个Object类型的数组(new Object[]{…}),如果参数中有原始类型的参数,会被转换成对应的包装类型。
3、$$
$$是方法所有参数的简写
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.makeClass("Javassist.SpecialVariables");
CtMethod ctMethod = new CtMethod(CtClass.voidType, "Test1",
new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("System.out.println($args);");
ctClass.addMethod(ctMethod);
//Test2方法调用Test1
CtMethod ctMethod1 = new CtMethod(CtClass.voidType, "Test2",
new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
ctMethod1.setModifiers(Modifier.PUBLIC);
ctMethod1.setBody("Test1($$);");
ctClass.addMethod(ctMethod1);
ctClass.writeFile();
}
这里在定义一个Test2方法调用Test1,传参时写成Test1($$)
就相当于Test1($1,$2)
剩下的遇到了再看吧。
Javassist 修改代码
Javassist 仅允许修改一个方法体中的表达式。javassist.expr.ExprEditor 是一个用来替换 方法体内表达式的类。用户可以定义 ExprEditor 的子类来制定表达式的修改
javassist.expr.MethodCall
当修改某个方法中的代码时,可以用MethodCall进行回环调用找到我们要改的函数,并通过replace()进行修改。
这里定义了一个print方法,并用到了print和println两个方法,之后通过MethodCall的getMethodName获取到该方法中调用的方法
所以这里可以做一个判断,当getMethodName等于print时既可以使用replace方法进行替换,由于只改方法不该参数,所以用$$直接代替原来的参数即可
package Javassist;
import javassist.*;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;
import java.io.IOException;
public class Demo04 {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.get("Javassist.Change");
CtMethod ctMethod = ctClass.getDeclaredMethod("print");
ctMethod.instrument(
new ExprEditor(){
public void edit(MethodCall m)
throws CannotCompileException{
if (m.getClassName().equals("java.io.PrintStream")
&&m.getMethodName().equals("print")){
m.replace("System.out.println($$);");
}
}
}
);
ctClass.writeFile();
}
}
class Change {
public static String name = "Sentiment";
public void print() {
System.out.println("This is one !");
System.out.print("This is two !");
}
}
javassist.expr.ConstructorCall
修改控制器的与上同理
参考链接
关于Java字节码编程javassist的详细介绍 | w3c笔记 (w3cschool.cn)
《宽字节安全》