基于javassist处理java字节码(二)
1 变量类型声明
编码时,如果我们需要在类中声明一个字段,或构造器或方法的参数,方法返回值,或在方法中声明一个局部变量,如果这个变量类型不在java.lang
package中,也不在当前类的package中,那么我们一般会先在类定义前先用import语句声明变量类型所属的package,然后就可以直接使用类名进行声明了。在javassist中,也可以通过ClassPool的importPackage()
方法简化变量类型声明,下面的例子展示使用Slf4j Logger打印日志:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("com.javatest.javassist.Animal");
pool.importPackage("org.slf4j.Logger");
pool.importPackage("org.slf4j.LoggerFactory");
CtField f = CtField.make("private static final Logger LOG = LoggerFactory.getLogger(com.javatest.javassist.Animal.class);", cc);
cc.addField(f);
CtMethod m = new CtMethod(CtClass.voidType, "printInfo", new CtClass[]{}, cc);
m.setModifiers(Modifier.PUBLIC);
m.setBody("{ LOG.info(\"use slf4j logger\"); }");
cc.addMethod(m);
对应的java文件内容如下所示:
package com.javatest.javassist;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Animal {
private static final Logger LOG = LoggerFactory.getLogger(Animal.class);
public void printInfo() {
LOG.info("use slf4j logger");
}
public Animal() {
}
}
如果不用importPackage()
,变量类型声明时可以使用类的全限定名(Full Qualified Name),如下所示:
CtField f = CtField.make("private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(com.javatest.javassist.Animal.class);", cc);
2 引用被编辑类的构造器或方法的上下文信息
我们在增强或编辑class时,需要引用构造器或方法的参数类型及参数值,方法的返回类型及返回值,以及参数或类的class对象,javassist定义了一些特殊的符号来获取或操作这些元素,这些特殊符号包括:
$0 : 代表类实例的this关键字,构造器和非静态方法才可以使用
$1,$2,$3... : 代表构造器和方法的第1、2、3。。。个参数值
$args : 代表构造器和方法的参数值数组,$args[0]表示第一个参数值,$args[1]表示第二个参数值,依次类推
$$ : 代表构造器和方法的参数值列表,用逗号分隔
$cflow(...) : 代表构造器和方法的for或while循环体,可以获取循环的次数等信息
$r : 代表方法的返回类型,主要用于类型转换
$w : 代表基本数据类型的wrapper类型,主要用于基本数据类型的wrap类型转换操作
$_ : 代表方法的返回,在insertAfter()中可以通过$_获取或修改返回值
$sig : 代表构造器和方法的参数Class对象数组
$type : 代表方法的返回值Class
$class : 代表当前类的Class
下面以一个例子说明如何引用上下文信息和编辑字节码
先定义一个类,全部内容如下:
package com.javatest.javassist;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Animal {
public static final Logger LOG = LoggerFactory.getLogger(Animal.class);
protected String name;
protected Integer count;
public Animal() {
this.name = "tom";
this.count = 0;
}
public Integer printInfo(Integer base) {
int i;
i = base;
i += 10;
return i;
}
}
下面编写一段测试代码来增加、修改上面定义的类的功能,关键点在代码中已经做了说明,测试代码如下:
public static void main(String []args) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass animalCc = pool.get("com.javatest.javassist.Animal");
CtClass strCc = pool.get("java.lang.String");
CtClass intCc = pool.get("java.lang.Integer");
// 创建一个两个参数的构造方法
CtConstructor cons = new CtConstructor(new CtClass[]{strCc, intCc}, animalCc);
// 通过$0引用this指针,$1,$2引用两个参数的值
cons.setBody("{ $0.name = $1; $0.count = $2;}");
animalCc.addConstructor(cons);
CtMethod method = animalCc.getDeclaredMethod("printInfo");
// 类中定义的字段,如LOG,可以直接引用
method.insertBefore("LOG.info(\"inserted before, base: \" + $1);");
// 在第21行前插入代码,修改类中的count字段的值,修改方法中定义的局部变量i的值,类字段和方法局部变量都可以直接引用
// 这个地方需要注意的是,因为 boxing/unboxing的原因,count加100不能直接写 count += 100,后面会解释
method.insertAt(21, "int b = 100 + count.intValue(); count = new Integer(b); i = i + b; LOG.info(\"count value: \" + count);");
method.addCatch("LOG.info(\"exception inf: {}\", $e); throw $e;", pool.get("java.io.IOException"));
// 通过 $_ 引用方法返回值
method.insertAfter("LOG.info(\"inserted before, returned value: {}\", $_);");
animalCc.writeFile("./javassist");
Class clazz = animalCc.toClass();
Object mouse = clazz.getConstructor(String.class, Integer.class).newInstance("Jerry", 10);
clazz.getDeclaredMethod("printInfo", Integer.class).invoke(mouse, 30);
} catch (Exception e) {
log.info("excepton: {}", ExceptionUtils.getStackTrace(e));
}
}
运行测试代码打印结果如下:
15:18:53.996 [main] INFO com.javatest.javassist.Animal - inserted before, base: 30
15:18:54.004 [main] INFO com.javatest.javassist.Animal - count value: 110
15:18:54.004 [main] INFO com.javatest.javassist.Animal - inserted before, returned value: 150
修改后的字节码反编译后的内容如下:
package com.javatest.javassist;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Animal {
public static final Logger LOG = LoggerFactory.getLogger(Animal.class);
protected String name;
protected Integer count;
public Animal() {
this.name = "tom";
this.count = 0;
}
public Integer printInfo(Integer base) {
Integer var10000;
try {
LOG.info("inserted before, base: " + base);
int i = base;
int var3 = 100 + this.count;
this.count = new Integer(var3);
i += var3;
LOG.info("count value: " + this.count);
i += 10;
var10000 = i;
} catch (IOException var7) {
LOG.info("exception inf: {}", var7);
throw var7;
}
Integer var6 = var10000;
LOG.info("inserted before, returned value: {}", var6);
return var6;
}
public Animal(String var1, Integer var2) {
this.name = var1;
this.count = var2;
}
}
3 override父类方法
实际应用中,使用javassist创建一个全新的类一般不太常见。在现有类上直接进行字节码修改或增强个人也不是特别推荐,因为对于一个特定的ClassLoader,同一个类只允许加载一次,如果直接修改,意味着被修改的原始类不能使用,而且只有一个修改版本。所以个人比较建议用javassist创建一个继承被修改类的类,然后对原始类的功能进行修改或增强。在子类中可以调用或覆写父类的方法,举例如下:
ClassPool pool = ClassPool.getDefault();
// 获取被修改类的字节码
CtClass animalCc = pool.get("com.javatest.javassist.Animal");
CtClass intCc = pool.get("java.lang.Integer");
// 创建一个新类,它继承被修改类
CtClass mouseCc = pool.makeClass("com.javatest.javassist.Mouse");
mouseCc.setSuperclass(animalCc);
// 添加一个新的字段
CtField f = CtField.make("private Integer age;", mouseCc);
mouseCc.addField(f);
// 为新类添加一个构造方法,构造体中调用父类的构造方法
CtConstructor cons = new CtConstructor(new CtClass[]{intCc}, mouseCc);
cons.setBody("{ super(); $0.age = $1;}");
mouseCc.addConstructor(cons);
// override父类的方法,在方法体中调用父类被覆盖的方法,同时增加其它的功能
CtMethod method = new CtMethod(intCc, "printInfo", new CtClass[]{intCc}, mouseCc);
method.setBody("{ LOG.info(\"inserted before, base: \" + $1); return super.printInfo($1);}");
mouseCc.addMethod(method);
4 boxing/unboxing
java编码时,因为java编译器提供了自动boxing/unboxing的语法糖,我们会经常不自觉的编写类似下面的代码:
Integer a = 100;
int b = a + 200;
但是由于javassist编译器并没有提供自动boxing/unboxing的语法糖,上面的写法javassist编译后的结果与我们预期的相去甚远,而且运行时会报异常。
上述语句javassist编译后的结果为:
Object var1 = true;
String var2 = String.valueOf(var1).concat(String.valueOf(200));
实现预期功能的代码应该如下编写,进行显式装箱和拆箱:
// 显式boxing
Integer a = new Integer(100);
// 显式unboxing,然后执行算术运算
int b = a.intValue() + 200;
所以在javassist环境中编码时需要特别注意以下两点:
1、只有基本类型可以进行算术运算,自动装箱类型不能直接进行算术运算
2、基本类型与相应的封装类型不能直接相互赋值,而是显式进行boxing/unboxing
5 可变参数方法
javassist并不直接支持可变参数,如果要生成可变参数方法,需要为方法添加Modifier.VARARGS
修饰符。例如:
ClassPool pool = ClassPool.getDefault();
CtClass animalCc = pool.makeClass("com.javatest.javassist.Animal");
CtMethod method = CtMethod.make("public int length(int[] args) { return args.length; }", animalCc);
method.setModifiers(method.getModifiers() | Modifier.VARARGS);
animalCc.addMethod(method);
会生成如下方法代码:
public int length(int... var1) {
return var1.length;
}
在javassist中调用可变参数方法也不能像java代码那样调用:
length(1, 2, 3);
而应该通过数组作为参数来调用:
length(new int[] { 1, 2, 3 });