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

2021-11-15  本文已影响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\");");

上一篇下一篇

猜你喜欢

热点阅读