基于javassist处理java字节码(二)

2021-11-17  本文已影响0人  生饼

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 });
上一篇下一篇

猜你喜欢

热点阅读