javassist笔记

457 阅读3分钟

1. 第一个简单的案例

第一个案例展示了给一个已存在的类 Student 新增成员变量 age1 age2,新增方法 getAge2 setAge2,并且实例化对象后 调用新增的方法

    @Test
    public void test1() throws Exception {

        // ClassPool是由装载了很多CtClass对象的HashTable组成
        ClassPool pool = ClassPool.getDefault();

        // 从class池中获取 Student类
        // 如果尝试获取一个 不存在的类,会抛出 javassist.NotFoundException
        CtClass ctClass = pool.get("com.maoxh.bytecode.javassist.other.Student");
        /**
         * 声明一个字段 age1 ,类型是 int
         * 参数1:字段类型 , CtClass.intType,doubleType....等8种基本数据类型 + 一个 voidType, 如果是对象类型,请参考第二个field
         * 参数2:字段名
         * 参数3:声明的类
         */
        CtField field1 = new CtField(CtClass.intType, "age1", ctClass);
        // 设置静态的私有的。 Modifier的类型是2的幂次,修饰类型的时候直接类型相加
        field1.setModifiers(Modifier.STATIC + Modifier.PRIVATE);
        // 声明一个字段 age2 ,类型是 Integer
        CtClass integerClass = pool.get(Integer.class.getName());
        CtField field2 = new CtField(integerClass, "age2", ctClass);
        field2.setModifiers(Modifier.PRIVATE);
        // 添加
        ctClass.addField(field1);
        ctClass.addField(field2);

        /**
         * 创建一个方法
         * 参数1:方法返回类型
         * 参数2:方法名
         * 参数3:参数列表
         * 参数4:声明的类
         */
        CtMethod getAgeMethod = new CtMethod(integerClass, "getAge2", new CtClass[]{}, ctClass);
        getAgeMethod.setModifiers(Modifier.PUBLIC);
        getAgeMethod.setBody("{ return age2;}");
        ctClass.addMethod(getAgeMethod);

        // 这里需要注意,字段 age2 是包装类Integer,所以在创建setAge2方法的时候,参数列表也必须是包装类,如果是int,则会出错
        CtMethod setAgeMethod = new CtMethod(CtClass.voidType, "setAge2", new CtClass[]{integerClass}, ctClass);
        setAgeMethod.setModifiers(Modifier.PUBLIC);
        /**
         * $1 = 第一个参数,也就是 Integer。 $2就是第二个参数,以此类推。$0=this
         */
        setAgeMethod.setBody("{ $0.age2 = $1;}");
        ctClass.addMethod(setAgeMethod);

        // 可以写入文件来查看生成的 CtClass
        // ctClass.writeFile("your path");

        Class<?> aClass = ctClass.toClass();
        Object o = aClass.newInstance();
        Method setAge2 = aClass.getDeclaredMethod("setAge2", Integer.class);
        setAge2.invoke(o, 20);

        Method getAge2 = aClass.getDeclaredMethod("getAge2");
        Object invoke = getAge2.invoke(o);
        System.out.println("age2=" + invoke);

    }

2. ClassPool

ClassPool是一个类池,主要由装载了很多CtClass对象的HashTable组成。操作字节码需要从ClassPool中获取CtClass对象.对象的装载是根据搜索路径来的。默认与jvm的搜索路径一致。如果你的类在默认路径找不到,你也可以自定义新增搜索路径,下面展示了几个方法来新增搜索路径。

同时javassist允许你自己创建ClassPool。下面的案例创建了一个 使用默认搜索路径的pool,以及创建了一个带有父pool的子ClassPool

@Test
public void classPool_test() throws Exception {
    // ClassPool.getDefault()方法的搜索路径和JVM的搜索路径是一致的。
    // 如果程序运行在JBoss或者Tomcat服务器上,
    // 那么ClassPool对象也许不能够找到用户类,原因是应用服务器用的是多个class loader
    ClassPool pool = ClassPool.getDefault();
    
    // 新增一个 /usr/local/javalib 作为搜索目录
    pool.insertClassPath("/usr/local/javalib");
    
    // 新增一个当前类的路径下面的类对象,也可以将 this.getClass替换为其他类
    pool.insertClassPath(new ClassClassPath(this.getClass()));
    
    // 也可以通过url 和 字节码
    // http://www.javassist.org/tutorial/tutorial.html

    // 此外还可以自己创建classPool,参数 true=使用默认path
    ClassPool mypool = new ClassPool(true);
    mypool.appendClassPath(new ClassClassPath(this.getClass()));

    // 此外还可以自己创建 一个及联的classPool,参数 pool=父pool
    ClassPool child = new ClassPool(pool);
    child.insertClassPath("./classes");
    // child的get,会先尝试从 父pool获取,如果获取不到,就从自己的path中获取
    CtClass ctClass = child.get("com.maoxh.bytecode.javassist.other.Student");
    // 也可以改变搜索行为,从子类先获取,然后查找父pool
    child.childFirstLookup = true;

}

3. CtField

3.1 通过构造器创建

以下的案例,展示操作新增Field,CtField field1 = new CtField(CtClass.intType, "age1", ctClass);

  • 参数1:类型 , CtClass.intType,doubleType....等8种基本数据类型 + 一个 voidType
  • 参数2:字段名
  • 参数3:声明的类

字段描述符javassist.Modifier的值是2的幂次,在设置描述符的时候,直接相加。

public static final int PUBLIC = 1;
public static final int PRIVATE = 2;
public static final int PROTECTED = 4;
public static final int STATIC = 8;
public static final int FINAL = 16;
public static final int SYNCHRONIZED = 32;
public static final int VOLATILE = 64;
public static final int VARARGS = 128;
public static final int TRANSIENT = 128;
public static final int NATIVE = 256;
public static final int INTERFACE = 512;
public static final int ABSTRACT = 1024;
public static final int STRICT = 2048;
public static final int ANNOTATION = 8192;
public static final int ENUM = 16384;

以下的例子将新增一个 private static final int age1; 的成员变量

ClassPool pool = ClassPool.getDefault();

CtClass ctClass = pool.get("com.maoxh.bytecode.javassist.other.Student");

CtField field1 = new CtField(CtClass.intType, "age1", ctClass);
field1.setModifiers(Modifier.STATIC + Modifier.FINAL + Modifier.PRIVATE);
ctClass.addField(field1);

如果成员变量类型不是基本数据类型呢?通过ClassPool.get(className)来获取类型

CtClass listCtClass = pool.get(List.class.getName());
CtField field2 = new CtField(listCtClass, "myList", ctClass);
ctClass.addField(field2);

3.2 通过CtField.make(src,CtClass)来创建

java.lang包下面的类无需导入,其他的类需要pool.importPackage后才能使用,否则会报编译错误

ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("com.maoxh.bytecode.javassist.other.Student");
pool.importPackage("java.util");
CtField make = CtField.make("private static Map myMap;", ctClass);
// 也可以使用全限定类名
// CtField make = CtField.make("private static java.util.Map myMap;", ctClass);
ctClass.addField(make);

4. CtMethod

4.1 新增方法

以下案例给Student类,新增一个add方法,返回两个int值的和。

一些说明

  • CtMethod构造器提供了4个参数
    • 参数1: 方法的返回类型
    • 参数2: 方法名
    • 参数3: 参数类型列表
    • 参数4: 声明的类
  • 方法体参数说明
    • $1代表第一个参数,$2代表第二个参数。。。以此类推
    • $0代表this,如果是static方法 $0 不可用
    • $$代表所有参数
    • $args代表参数列表,是一个数组
    • $w 代表包装类型。必须在转义表达式中用于类型转换。($w)将基础类型转换为对应的包装类型
    • $r 代表着结果类型,必须在转换表达式中用作类型转换
    • $cflow
    • $_
    • $sig 代表java.lang.Class对象数组,代表正式的参数类型
    • $class代表 java.lang.Class对象,代表当前操作的方法(等价$0的类型)
    • $type ,java.lang.Class对象,代表正式的结果类型.
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("com.maoxh.bytecode.javassist.other.Student");

/**
 * 创建一个方法
 * 参数1:方法返回类型
 * 参数2:方法名
 * 参数3:参数列表
 * 参数4:声明的类
 */
CtMethod add = new CtMethod(CtClass.intType, "add", new CtClass[]{CtClass.intType,CtClass.intType}, ctClass);
add.setModifiers(Modifier.PUBLIC);
add.setBody("{ return $1+$2;}");
ctClass.addMethod(add);

Class<?> aClass = ctClass.toClass();
Object o = aClass.newInstance();
Method addMethod = aClass.getDeclaredMethod("add", int.class, int.class);
Object res = addMethod.invoke(o, 33 , 66);
System.out.println(res); 

也可以使用CtMethod.make静态方法来创建method,以下案例展示了一个除法运算,同时增加了trycatch代码块,当异常时固定返回1。(需要注意的是,下面代码块注释了一行代码,这一行因为使用了可变参数而导致编译不通过。javassist不支持可变参数,如需要可用数组代替)

ClassPool pool = ClassPool.getDefault();

CtClass ctClass = pool.get("com.maoxh.bytecode.javassist.other.Student");
//     CtMethod add = CtMethod.make("public Integer add(int ...a){int result = 0;" +
//     "for(int k =0;k<a.length;k++){System.out.println($args[k]); result += a[k];} return ($r)result;" +
//     "}", ctClass);
CtMethod add = CtMethod.make("public int division(int a,int b){int c = a / b;  return c;}", ctClass);
add.setModifiers(Modifier.PUBLIC);
add.addCatch("{System.out.println($e); return 1;}", pool.get("java.lang.Exception"));
ctClass.addMethod(add);

4.2 修改方法

以下案例展示了如何修改已有方法,在方法开头打印了参数,在方法结尾打印了结果,其中insertAfter第二个参数代表是否finally的。在下面的案例我们可以看到$_代表的是返回值,$args[1]等价$2

ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("com.maoxh.bytecode.javassist.other.Student");
CtMethod multiply = ctClass.getDeclaredMethod("multiply");

multiply.insertBefore("{System.out.println($1);System.out.println($args[1]);}");

multiply.insertAfter("{System.out.println($_);}", true);
Class<?> aClass = ctClass.toClass();
Student o = (Student)aClass.newInstance();
o.multiply(4,5);

5. 拆箱和装箱

在Java中,装箱和拆箱操作是语法糖。对于字节码说来,是不存在装箱和拆箱的。所以Javassist的编译器不支持装箱拆箱操作。

Integer i = new Integer(1); // 在java和javassist中允许
Integer i = 1; // 在java允许 。javassist不允许

6. 冻结与解冻

当CtClass对象writeFile(),toClass()或者toBytecode()转换成了类对象,Javassist将会冻结CtClass对象。此后任何对次对象的操作都是不允许的。之所以这样做,主要是因为此类已经被JVM加载,由于JVM本身不支持类的重复加载操作,所以不允许更改。

如果需要再次修改,需要调用CtClass.defrost()

ClassPool pool = ClassPool.getDefault();
CtClass studentClass = pool.get("com.maoxh.bytecode.javassist.other.Student");

// studentClass.writeFile();
// Class<?> aClass = studentClass.toClass();
// byte[] bytes = studentClass.toBytecode();
/**
 * 调用了defrost()方法之后,CtClass对象就可以随意修改了。
 */
studentClass.defrost();
CtField field1 = new CtField(CtClass.intType, "age1", studentClass);
studentClass.addField(field1);

7. 官方文档

官方文档

8. 案例中使用到的Student类

public class Student {

    private String name;

    private Integer age;

    private String address;


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public int multiply(int a, int b){
        int c= a * b;
        return c;
    }
}