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

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

作者: 生饼 | 来源:发表于2021-11-15 19:32 被阅读0次

    0 前言

    为了用更少的代码响应多样的、易变的外部需求,java提供了运行时生成、修改、增强java类字节码的能力,这一项能力在很多框架(如spring framework)、中间件(如hikariCP)软件中大放异彩。相比于ASM(assemble的缩写,名称来自于C语言的asm关键字)、CGLIB(Code Generation LIBrary)等老牌且广泛流行的字节码查看和编辑工具,javassist(Java Programming Assistant)提供了更易于学习、使用的接口和方式来处理java字节码。使用者通过自己非常熟悉的java语言代码、基于类对象交互方式来操作字节码,从而屏蔽了底层class文件的结构细节,就像开发普通程序一样实现字节码编辑的高级功能。javassist极大的提高了基于字节码开发的效率,降低了学习曲线,且保证了较高的性能。性能仅略低于ASM,高于CGLIB,远远高于JDK自带的动态代理(dynamic proxy,几十倍的差距)

    1 javassist包

    要使用javassist,只要在项目中添加相应的依赖即可,maven依赖(当前最新版本是3.28.0-GA)如下:

            <!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
            <dependency>
                <groupId>org.javassist</groupId>
                <artifactId>javassist</artifactId>
                <version>3.28.0-GA</version>
            </dependency>
    

    2 创建一个类

    下面的代码创建了一个Animal类,并给这个类添加了一个name字段,以及name字段的setter()getter()方法,同时分别添加了一个无参和有参构造函数,添加了一个void printName()方法,实现打印Animal类对象name字段值的功能。最后将创建的类写入class file文件

    package com.javatest.javassist;
    
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtConstructor;
    import javassist.CtField;
    import javassist.CtMethod;
    import javassist.CtNewMethod;
    import javassist.Modifier;
    
    
    public class JavassistMain {
    
        public static void main(String []args) {
    
            try {
             // 1. ClassPool相当于一个存储、管理javassist class字节码的容器
            ClassPool pool = ClassPool.getDefault();
    
            // 2. 创建一个空类,类的全限定名为 com.javatest.javassist.Animal
            CtClass cc = pool.makeClass("com.javatest.javassist.Animal");
    
            // 3. 新增一个字段
            CtField nameField = CtField.make("private String name;", cc);
            cc.addField(nameField);
    
            // 4. 添加无参的构造函数
            CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
            // 构造函数的内容
            cons.setBody("{name = \"tiger\";}");
            cc.addConstructor(cons);
    
            // 5. 添加有参的构造函数
            cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
            // $0=this / $1,$2,$3... 代表方法参数
            cons.setBody("{$0.name = $1;}");
            cc.addConstructor(cons);
    
            // 6. 添加字段的getter、setter方法
            cc.addMethod(CtNewMethod.setter("setName", nameField));
            cc.addMethod(CtNewMethod.getter("getName", nameField));
    
            // 7. 创建一个名为printName方法,无参数,无返回值,输出name值
            CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
            ctMethod.setModifiers(Modifier.PUBLIC);
            ctMethod.setBody("{System.out.println(name);}");
            cc.addMethod(ctMethod);
    
            // 8. 生成class file文件,写入项目当前工作目录下
            cc.writeFile("./");
    
            } catch (Exception e) {
                // 
            }
    
        }
    }
    

    执行上面的代码,会在当前目录的子目录com/javatest/javassist下生成一个名为Animal.class的文件,通过反编译可查看class文件对应的代码如下:

    package com.javatest.javassist;
    
    public class Animal {
        private String name;
    
        public Animal() {
            this.name = "tiger";
        }
    
        public Animal(String var1) {
            this.name = var1;
        }
    
        public void setName(String var1) {
            this.name = var1;
        }
    
        public String getName() {
            return this.name;
        }
    
        public void printName() {
            System.out.println(this.name);
        }
    }
    

    3 加载使用创建的class

    class完成编辑后,我们可以像上面例子一样将字节码写入class file中:

    cc.writeFile("./");
    

    也可以转成字节码序列,提供给应用程序其他部分使用(比如一个类加载器)或通过网络发送给一个远程服务,下面的例子跟上面的效果相同

        // 转换成字节码
        byte[] b = cc.toBytecode();
        OutputStream o = new FileOutputStream("./javassist");
        o.write(b);
        o.close();
    

    或者通过当前线程的上下文类加载器直接将CtClass代表的class file加载到JVM中:

        Class clazz = cc.toClass();
    

    这样我们可以通过反射的方式创建类的实例和调用实例方法:

        Object tiger = clazz.newInstance();
        Method method = clazz.getMethod("printName");
        method.invoke(tiger);
    

    但是通过反射调用一方面编码比较繁琐,性能也不理想,更好的方式是先定义一个接口:

    package com.javatest.javassist;
    
    public interface AnimalPrinter {
    
        void printName();
    }
    

    然后让创建的class实现这个接口,在上面的例子中增加创建CtClass后设置它实现的接口:

        CtClass cc = pool.makeClass("com.javatest.javassist.Animal");
        cc.addInterface(pool.get("com.javatest.javassist.AnimalPrinter"));
    

    然后便可以通过接口直接调用

        AnimalPrinter printer = (AnimalPrinter) clazz.newInstance();
        printer.printName();
    

    4 javassist基本使用方法

    我们知道,一个java类由类声明本身、字段、构造函数、方法等元素组成。从上面的基本例子可以看出,javassist为java类的这些组成元素分别设计了相应的类CtClass、CtField、CtConstructor、CtMethod,我们就是通过这些类来处理java class字节码的。这些类名的前缀Ctcompile time的缩写,表示这些类代表的是javassist管理的编译时的字节码,需要加载到JVM中才能使用。

    4.1 ClassPool

    ClassPool用来存储和管理class字节码对象,它相当于一个容器,里面维护了一个Map,key为class的全限定名,value为CtClass对象。熟悉spring的朋友可以用spring容器这个概念来做类比。

    我们可以通过静态方法ClassPool.getDefault()获取一个单例的ClassPool对象,也可以通过ClassPool pool = new ClassPool()创建新的ClassPool对象;如果需要,我们还可以创建一个ClassPool链,这样可以重用一些ClassPool的内容,如下所示:

    ClassPool parent = new ClassPool();
    ClassPool child = new ClassPool(parent)
    

    4.2 CtClass

    4.2.1 创建CtClass对象,并添加到ClassPool中

    我们可以通过ClassPool的makeClass()系列方法创建一个类的CtClass对象并自动添加到ClassPool中,同样的,可以通过ClassPool的makeInterface()系列方法创建一个接口的CtClass对象。典型方法举例如下:

    CtClass makeClass(InputStream classfile);
    CtClass makeClass(ClassFile classfile);
    CtClass makeClass(String classname);
    CtClass makeClass(String classname, CtClass superclass);
    CtClass makeInterface(String name);
    CtClass makeInterface(String name, CtClass superclass);
    

    我们更经常使用ClassPool的get(String classname)方法获取CtClass对象,get()方法传入的参数是类的全限定名,ClassPool会先在自己当中查找相应的CtClass对象,如果不存在,则会到ClassPool配置的类搜索路径(class search path)中查找相应的class file,然后创建CtClass对象并加载到ClassPool中。

    当我们像上面的例子中那样通过ClassPool pool = ClassPool.getDefault()方式获取ClassPool对象时,pool中已经添加了系统类搜索路径(system search path),系统类搜索路径包括JVM platform库、扩展库、以及应用程序的CLASSPATH路径,所以如果为pool添加了系统类搜索路径,我们可以通过改变应用程序的CLASSPATH从而改变class搜索路径。我们还可以ClassPool pool = new ClassPool(true)方式在创建ClassPool对象时为pool添加系统类搜索路径。或者像下面这样是同样的效果:

    ClassPool pool = new ClassPool();
    // 为pool添加系统类搜索路径
    pool.appendSystemPath();
    

    在某些环境下,如Web容器、OSGI等,应用有多个类加载器(ClassLoader),这时可能需要添加相应的类搜索路径,我们还可以通过ClassPool提供的以下方法添加:

    ClassPath appendClassPath(ClassPath cp);
    ClassPath insertClassPath(ClassPath cp);
    ClassPath appendClassPath(String pathname);
    ClassPath insertClassPath(String pathname);
    
    

    例如,假设我们有一个类实例Aninal cat,我们希望ClassPool能加载cat类加载器相应加载路径下的class,可以如下为pool添加类搜索路径:

    ClassPool pool = ClassPool.getDefault();
    pool.insertClassPath(new LoaderClassPath(cat.getClass().getClassLoader()));
    

    4.2.2 CtClass基本操作

    我们可以通过CtClass的setSuperclass(CtClass clazz)方法为类设置父类,通过setInterfaces(CtClass[] list)addInterface(CtClass anInterface)方法为类添加实现的接口,通过setModifiers(int mod)方法设置类的修饰符,通过setName()修改类名,例如:

    ClassPool pool = ClassPool.getDefault();
    CtClass animal = pool.makeClass("com.javatest.javassist.Animal");
    CtClass cat = pool.makeClass("com.javatest.javassist.Cat");
    cat.setSuperclass(animal);
    cat.setModifiers(Modifier.PUBLIC | Modifier.FINAL);
    // 将Cat类修改为Dog
    cat.setName("Dog");
    

    我们可以通过以下系列方法为CtClass添加和删除字段、构造器、方法:

    addField()
    addConstructor()
    addMethod()
    removeField()
    removeConstructor()
    removeMethod()
    

    当我们调用了CtClass对象的writeFile()toClass()toBytecode()等方法,javassist会冻结相应的CtClass;或者如果我们的CtClass已经设计好了,也可以主动通过freeze()方法将CtClass冻结,避免意外修改了CtClass。当然,如果我们确实需要重新修改CtClass,可以通过defrost()方法将CtClass解冻;如果创建的CtClass不再使用了,比如已经加载到了JVM中,可以通过detach()方法释放CtClass在ClassPool中占用的资源。

    ClassPool pool = ClassPool.getDefault();
    CtClass animal = pool.makeClass("com.javatest.javassist.Animal");
    
    animal.freeze();
    // 无法执行,抛出异常
    animal.setModifiers(Modifier.FINAL);
    
    animal.defrost();
    // 可正常执行
    animal.setModifiers(Modifier.FINAL);
    
    // 释放相关资源
    animal.detach();
    
    

    4.3 CtField

    我们可以通过CtField的静态方法make()或new一个新的CtField实例来创建CtField对象,CtField的基本使用方法和说明如下例子所示:

        ClassPool pool = ClassPool.getDefault();
        CtClass animal = pool.makeClass("com.javatest.javassist.Animal");
    
        // 创建一个名字为name的field,可以看到跟我们手写代码是一模一样的
        CtField field = CtField.make("private String name;", animal);
    
        // 下面两行代码的效果跟上面是一样的
        // CtField field = new CtField(pool.get("java.lang.String"), "name", animal);
        // nameField.setModifiers(Modifier.PRIVATE);
    
        // 下面两行的效果相当于删除了Animal类的name字段,添加了一个类型为long的age字段
        // 修改字段的名字
        field.setName("age");
        // 修改字段的类型
        field.setType(CtClass.longType);
    
        // 添加到CtClass中
        animal.addField(field);
        // 添加到CtClass中, 并初始化值为60
        // animal.addField(field, "60L");
        // animal.addField(field, CtField.Initializer.constant(60L));
    

    4.4 CtConstructor

    下面的例子展示了为类添加一个无参构造器的方法,有参构造器只要提供一个参数CtClass列表即可:

        ClassPool pool = ClassPool.getDefault();
        CtClass animal = pool.makeClass("com.javatest.javassist.Animal");
    
        CtField field = CtField.make("private String name;", animal);
        animal.addField(field);
    
        // 创建一个无参构造器
        CtConstructor cons = new CtConstructor(new CtClass[]{}, animal);
        // 构造器的方法体,多次调用时,会整体替换已经存在的body内容
        cons.setBody("{name = \"Tom\";}");
        animal.addConstructor(cons);
    
        // 在构造器的body的最前面添加内容
        cons.insertBeforeBody("System.out.println(\"====this is constructor\");");
    

    从上面的例子可以看出,构造器的body内容以及新插入的代码跟我们平常开发代码是一样的。不过需要注意的是,setBody()的内容需要用{}包裹起来

    CtNewConstructor工厂类则提供了一些方便的方法来创建构造函数:

    // copy其他类的构造方法
    CtConstructor copy(CtConstructor c, CtClass declaring,ClassMap map);
    // 默认构造方法
    CtConstructor defaultConstructor(CtClass declaring);
    // make方法系列
    CtConstructor make(String src, CtClass declaring);
    CtConstructor make(CtClass[] parameters,CtClass[] exceptions, CtClass declaring);
    CtConstructor make(CtClass[] parameters,CtClass[] exceptions,String body, CtClass declaring);
    CtConstructor make(CtClass[] parameters,
                        CtClass[] exceptions, int howto,
                        CtMethod body, ConstParameter cparam,
                        CtClass declaring);
    

    下面的例子创建的构造方法与上面是一样的:

        CtConstructor cons = CtNewConstructor.make("public Animal() {name = \"Tom\";}", animal);
        animal.addConstructor(cons);
    
    

    4.5 CtMethod

    4.5.1 创建CtMethod

    跟创建构造器类似,我们可以new CtMethod()或使用CtNewMethod的工厂方法创建新的类,稍不同的是方法需要提供方法名和返回值类型:

        ClassPool pool = ClassPool.getDefault();
        CtClass animal = pool.makeClass("com.javatest.javassist.Animal");
    
        // 创建一个 void printInfo() 方法
        CtMethod printInfo = new CtMethod(CtClass.voidType, "printInfo", new CtClass[]{}, animal);
        printInfo.setModifiers(Modifier.PUBLIC);
        printInfo.setBody("{System.out.println(\"====this is constructor\");}");
        animal.addMethod(printInfo);
    
    

    CtNewMethod提供工厂方法主要有:

    // 字段的getter、setter方法
    CtMethod getter(String methodName, CtField field);
    CtMethod setter(String methodName, CtField field);
    // 抽象方法
    CtMethod abstractMethod(CtClass returnType,
                            String mname,
                            CtClass[] parameters,
                            CtClass[] exceptions,
                            CtClass declaring);
    CtMethod copy(CtMethod src, CtClass declaring,ClassMap map);
    CtMethod copy(CtMethod src, String name, CtClass declaring,ClassMap map);
    // make系列
    CtMethod make(String src, CtClass declaring);
    CtMethod make(String src, CtClass declaring,String delegateObj, String delegateMethod);
    CtMethod make(CtClass returnType,
                  String mname, CtClass[] parameters,
                  CtClass[] exceptions,
                  String body, CtClass declaring);
    CtMethod make(int modifiers, CtClass returnType,
                  String mname, CtClass[] parameters,
                  CtClass[] exceptions,
                  String body, CtClass declaring);
    

    4.5.2 编辑CtMethod方法体内容

    除了通过setBody()方法或CtNewMethod.make()系列工厂方法一次提供方法的全部内容,CtMethod提供了一系列丰富的用来编辑方法内容的方式,主要的几个方法如下所示:

    // 修改方法名字
    setName();
    // 添加方法参数
    insertParameter();
    addParameter();
    // 在方法体中插入代码
    insertBefore();
    insertAfter();
    insertAt();
    addCatch();
    

    举一个简单的例子

        ClassPool pool = ClassPool.getDefault();
        CtClass animal = pool.makeClass("com.javatest.javassist.Animal");
    
        // 创建一个 void printInfo() 方法
        CtMethod printInfo = new CtMethod(CtClass.voidType, "printInfo", new CtClass[]{}, animal);
        printInfo.setModifiers(Modifier.PUBLIC);
        printInfo.setBody("{System.out.println(\"====this is a method\");}");
        animal.addMethod(printInfo);
    
        // 在方法入口处插入代码
        printInfo.insertBefore("System.out.println(\"inserted at method entry point\");");
        // 在方法所有返回点插入代码
        printInfo.insertAfter("System.out.println(\"inserted before method return\");");
        // 在指定行插入代码
        // printInfo.insertAt(10, "System.out.println(\"insert at dedicated line\");");
    
    

    相关文章

      网友评论

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

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