美文网首页
基于javassist处理java字节码(二)

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

作者: 生饼 | 来源:发表于2021-11-17 11:25 被阅读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 });
    

    相关文章

      网友评论

          本文标题:基于javassist处理java字节码(二)

          本文链接:https://www.haomeiwen.com/subject/dygqtrtx.html