javassist介绍

(一)Javassist是什么
Javassist是可以动态编辑Java字节码的类库。它可以在Java程序运行时定义一个新的类,并加载到JVM中;还可以在JVM加载时修改一个类文件。Javassist使用户不必关心字节码相关的规范也是可以编辑类文件的。
使用流程:
0
 
(二)Javassist核心API
在Javassist中每个需要编辑的class都对应一个CtCLass实例,CtClass的含义是编译时的类(compile time class),这些类会存储在Class Pool中(Class pool是一个存储CtClass对象的容器)。 CtClass中的CtField和CtMethod分别对应Java中的字段和方法。通过CtClass对象即可对类新增字段和修改方法等操作了。
0
1. ClassPool:javassist的类池,使用ClassPool 类可以跟踪和控制所操作的类,它的工作方式与 JVM 类装载器非常相似,常见方法列表:
  1. getDefault : 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
  2. appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
  3. toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。
  4. 需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
  5. get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑。
2. CtClass: CtClass提供了类的操作,如在类中动态添加新字段、方法和构造函数、以及改变类、父类和接口的方法。,常见方法列表:
  1. freeze : 冻结一个类,使其不可修改;
  2. isFrozen : 判断一个类是否已被冻结;
  3. prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
  4. defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
  5. detach : 将该class从ClassPool中删除;
  6. writeFile : 根据CtClass生成 .class 文件;
  7. toClass : 通过类加载器加载该CtClass。
上面我们创建一个新的方法使用了CtMethod类。CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。
3. CtField:类的属性,通过它可以给类创建新的属性,还可以修改已有的属性的类型,访问修饰符等
4. CtMethod:类中的方法,通过它可以给类创建新的方法,还可以修改返回类型,访问修饰符等, 甚至还可以修改方法体内容代码。一些重要的方法:
  1. insertBefore : 在方法的起始位置插入代码;
  2. insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
  3. insertAt : 在指定的位置插入代码;
  4. setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
  5. make : 创建一个新的方法。
注意到在上面代码中的:setBody()的时候我们使用了一些符号:
Copy// $0=this / $1,$2,$3... 代表方法参数 cons.setBody("{$0.name = $1;}");
具体还有很多的符号可以使用,但是不同符号在不同的场景下会有不同的含义,所以在这里就不在赘述,可以看javassist 的说明文档。http://www.javassist.org/tutorial/tutorial2.html
5. CtConstructor:与CtMethod类似
API运用
ClassPool
//ClassPool
// 类库, jvm中所加载的class
ClassPool pool = ClassPool.getDefault();
// 加载一个已知的类, 注:参数必须为全量类名
CtClass ctClass = pool.get("com.dxz.dto.Person");
// 创建一个新的类, 类名必须为全量类名
CtClass tClass = pool.makeClass("com.dxz.Hello");
CtField
//CtField
// 获取已知类的属性
CtField ctField = ctClass.getDeclaredField("name");
// 构建新的类的成员变量
CtField ctFieldNew = new CtField(CtClass.charType, "address", ctClass);
// 构建新的类的成员变量
CtField ctFieldNew2 = new CtField(CtClass.intType, "phone", ctClass);
// 设置类的访问修饰符为public
ctFieldNew.setModifiers(Modifier.PUBLIC);
// 设置类的访问修饰符为public
ctFieldNew2.setModifiers(Modifier.PUBLIC);
// 将属性添加到类中
ctClass.addField(ctFieldNew);
// 将属性添加到类中
ctClass.addField(ctFieldNew2);
CtMethod
//CtMethod
// 获取已有方法
//CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello");

//创建新的方法, 参数1:方法的返回类型,参数2:名称,参数3:方法的参数,参数4:方法所属的类
CtMethod ctMethod = new CtMethod(CtClass.intType, "calc", new CtClass[]
        {CtClass.intType, CtClass.intType}, tClass);
// 设置方法的访问修饰
ctMethod.setModifiers(Modifier.PUBLIC);
// 将新建的方法添加到类中
ctClass.addMethod(ctMethod);
// 方法体内容代码 $1代表第一个参数,$2代表第二个参数
ctMethod.setBody("return $1 + $2;");

//ctClass.addMethod(ctMethod);
CtConstructor
// 获取已有的构造方法, 参数为构建方法的参数类型数组
CtConstructor ctConstructor = ctClass.getDeclaredConstructor(new CtClass[]{});
// 创建新的构造方法
CtConstructor ctConstructor = new CtConstructor(new CtClass[]{CtClass.intType},ctClass); ctConstructor.setModifiers(Modifier.PUBLIC);
ctConstructor.setBody("this.age = $1;");
ctClass.addConstructor(ctConstructor);
// 也可直接创建
ctConstructor = CtNewConstructor.make("public Student(int age){this.age=age;}", ctClass);

 

(三)javassist.bytecode.ClassFileWriter核心API
Javassist提供了低级API来直接编辑类文件。为了使用这些API,你需要详细了解Java字节码和类文件的格式,这样你就可以通过这些API对类文件进行各种修改。
如果你想要产生一个简单的类文件,javassist.bytecode.ClassFileWriter可能提供了最好的API。它提供了比javassist.bytecode.ClassFile更快的速度,尽管这个API更小一些。
1. 获取ClassFile对象
javassist.bytecode.ClassFile对象表示一个类文件。为了获取这个对象,应该调用CtClass的getClassFile()。 除此之外,你可以直接从类文件构造一个javassit.bytecode.ClassFile。例如,
BufferedInputStream fin
        = new BufferedInputStream(new FileInputStream("Point.class"));
ClassFile cf = new ClassFile(new DataInputStream(fin));
这些代码片段从Point.class创建了一个ClassFile对象。
一个ClassFile对象可以被写回类文件。ClassFile的write()将类文件的内容写入DataOutputStream。
2. 添加和移除成员
ClassFile提供了addField()和addMethod()来添加字段或方法(注意构造函数在字节码级别被当作普通的方法)。它提供addAttribute()来添加一个属性到类文件。
注意FieldInfo,MethodInfo,AttributeInfo对象包括一个到ConstPool(常量池表)引用。ConstPool对象必须是ClassFile对象和FieldInfo(或MethodInfo等)的公用对象,该对象被添加到这些ClassFile对象中。换句话说,FieldInfo(或MethodInfo等)对象禁止在不同的ClassFile对象中共享。
为了从ClassFile对象中移除字段或方法,你必须先获取一个包含了类所有字段的java.util.List对象。getFields()和getMethods()返回一个列表。字段或方法可以通过调用List对象的remove()方法移除。属性也可以用相似的方式移除。调用FieldInfo或MethodInfo的getAttributes()获取一个属性列表,然后从列表中移除。
3. 遍历方法体
为了检查方法体中所有的字节码指令,CodeIterator非常有效。为了获取这个对象,按下面的方式来做:
MethodInfo minfo = cf.getMethod("move");    // we assume move is not overloaded.
CodeAttribute ca = minfo.getCodeAttribute();
CodeIterator ci = ca.iterator();
CodeIterator对象允许你从头到尾逐条访问字节码指令。下面的方法是CodeIterator声明的部分方法:
  • void begin():移动到第一条指令。
  • void move(int index):移动到指定索引的指令。
  • boolean hasNext():如果还有指令,返回true。
  • int next():返回下一条指令的索引。注意,它不返回下一条操作码的索引。
  • int byteAt(int index):返回指定索引的正8位值。
  • int u16bitAt(int index):返回指定索引的正16位值。
  • int write(byte[] code, int index):将byte数组写入到指定索引。
  • void insert(int index, byte[] code):插入byte数组到索引。分支偏移量等被自动调整。
下面的代码片段展示了方法体中包含的所有指令:
while (ci.hasNext()) {
    int index = ci.next();
    int op = ci.byteAt(index);
    System.out.println(Mnemonic.OPCODE[op]);
}

 

完整示例:
package com.dxz;

import javassist.bytecode.*;

import java.io.*;

public class Demo2 {
    public static void main(String[] args) throws IOException, BadBytecode {
        BufferedInputStream fin
                = new BufferedInputStream(new FileInputStream("D:\\study\\javaagent\\javassist-demo\\build\\classes\\java\\main\\com\\dxz\\dto\\Person.class"));
        ClassFile cf = new ClassFile(new DataInputStream(fin));

        MethodInfo minfo = cf.getMethod("sayHello");    // we assume move is not overloaded.
        CodeAttribute ca = minfo.getCodeAttribute();
        CodeIterator ci = ca.iterator();

        while (ci.hasNext()) {
            int index = ci.next();
            int op = ci.byteAt(index);
            System.out.println(Mnemonic.OPCODE[op]);
        }
    }
}

 

结果:
> Task :Demo2.main()
getstatic
new
dup
invokespecial
ldc
invokevirtual
aload_0
getfield
invokevirtual
ldc
invokevirtual
aload_0
getfield
invokevirtual
invokevirtual
invokevirtual
return

 

4. 产生字节码序列
Bytecode对象表示字节码指令的序列。它是字节码的可变数组。示例代码如下:
ConstPool cp = ...;    // constant pool table
Bytecode b = new Bytecode(cp, 1, 0);
b.addIconst(3);
b.addReturn(CtClass.intType);
CodeAttribute ca = b.toCodeAttribute();
这产生了如下序列:
iconst_3
ireturn
你也可以通过调用Bytecode的get()方法获取包含这个序列的字节数组。获取的数组可以被插入到另一个代码属性中。
Bytecode提供了大量方法来添加特殊的指令到序列中,它提供addOpcode()来添加8位操作码和addIndex()来添加一个索引。每一个操作码的8位值都在Opcode接口中定义。
用来添加特殊指令的addOpcode()和其它方法是自动维护最大栈深的,除非控制流不包含分支。这个值可以通过调用Bytecode对象的getMaxStack()获取。它也反应在由ByteCode对象创建的CodeAttribute对象中。为了重新计算方法体的最大栈深,调用CodeAttribute的computeMaxStack()方法。
5. 注解(元标记)
注解存储在类文件中,作为运行时不可见(或可见)的注解属性。这些属性可以从ClassFile,MethodInfo,FieldInfo对象中获取。调用这些对象的getAttribute(AnnotationsAttribute.invisibleTag)。更详细的说明,可以查看javassist.bytecode.AnnotationsAttribute类和javassist.bytecode.annotation包的javadoc手册。
Javassist也允许你通过高级的API访问注解。如果你想要通过CtClass访问注解,调用CtClass或者CtBehavior的getAnnotations()方法。
(四)、调用生产的类对象
1. 通过反射的方式调用
通过javassist创建一个类对象然后输出该对象编译完之后的 .class 文件。那如果我们想调用生成的类对象中的属性或者方法应该怎么去做呢?javassist也提供了相应的api,生成类对象的代码还是和第一段一样,将最后写入文件的代码替换为如下:
package com.dxz;

import javassist.*;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Call1 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, NotFoundException, CannotCompileException, InstantiationException {
        ClassPool pool = ClassPool.getDefault();

        // 1. 创建一个空类
        CtClass cc = pool.makeClass("com.dxz.dto.Compony");

        // 2. 新增一个字段 private String name;
        // 字段名为name
        CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
        // 访问级别是 public (setName方法的访问级别)
        param.setModifiers(Modifier.PUBLIC);
        // 初始值是 "xiaoming"
        cc.addField(param, CtField.Initializer.constant("xiaoming"));

        // 3. 生成 getter、setter 方法
        cc.addMethod(CtNewMethod.setter("setName", param));
        cc.addMethod(CtNewMethod.getter("getName", param));

        // 4. 添加无参的构造函数
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
        cons.setBody("{name = \"xiaohong\";}");
        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. 创建一个名为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);
        // 这里不写入文件,直接实例化
        Object person = cc.toClass().newInstance();

        //下面通过反射的方式调用
        // 设置值
        Method setName = person.getClass().getMethod("setName", String.class);
        setName.invoke(person, "spring");
        // 输出值
        Method execute = person.getClass().getMethod("printName");
        execute.invoke(person);
    }
}
然后执行main方法就可以看到调用了 printName方法。结果:
0
2. 通过读取 .class 文件的方式调用
package com.dxz;

import javassist.*;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Call2 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, NotFoundException, CannotCompileException, InstantiationException {
        ClassPool pool = ClassPool.getDefault();
        // 设置类路径
        pool.appendClassPath("D:\\study\\javaagent\\javassist-demo\\src\\main\\java\\");
        CtClass ctClass = pool.get("com.dxz.dto.Person");
        // 6. 创建一个名为printName方法,无参数,无返回值,输出name值
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, ctClass);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(name);}");
        ctClass.addMethod(ctMethod);

        // 这里不写入文件,直接实例化
        Object person = ctClass.toClass().newInstance();
        //下面通过反射的方式调用
        // 设置值
        Method setName = person.getClass().getMethod("setName", String.class);
        setName.invoke(person, "spring call2");
        // 输出值
        Method execute = person.getClass().getMethod("printName");
        execute.invoke(person);
    }
}
结果:
0
3. 通过接口的方式
上面两种其实都是通过反射的方式去调用,问题在于我们的工程中其实并没有这个类对象,所以反射的方式比较麻烦,并且开销也很大。那么如果你的类对象可以抽象为一些方法得合集,就可以考虑为该类生成一个接口类。这样在newInstance()的时候我们就可以强转为接口,可以将反射的那一套省略掉了。
示例:
package com.dxz.dto;

public interface IStudent {
    void setName(String name);
    String getName();
    void printName();
}
package com.dxz.dto;

public class GoodStudent {
    private String name;
    private int grade;
    public GoodStudent() {
        this.name = "good";
    }
    //get set省
}
package com.dxz;

import com.dxz.dto.IStudent;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;

public class call3 {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
        ClassPool pool = ClassPool.getDefault();
        pool.appendClassPath("D:\\study\\javaagent\\javassist-demo\\src\\main\\java");

        // 获取接口
        CtClass codeClassI = pool.get("com.dxz.dto.IStudent");
        // 获取上面生成的类
        CtClass ctClass = pool.get("com.dxz.dto.GoodStudent");
        // 使代码生成的类,实现 IPerson 接口
        ctClass.setInterfaces(new CtClass[]{codeClassI});

        // 以下通过接口直接调用 强转
        IStudent person = (IStudent) ctClass.toClass().newInstance();
        System.out.println(person.getName());
        person.setName("xiaohuihui");
        person.printName();
    }
}
结果:
0
(五)、总结
javassist被用于struts2和hibernate中,都用来做动态字节码修改使用。一般开发中不会用到,但在封装框架时比 较有用。虽然javassist提供了一套简单易用的API,但如果用于平常的开发,会有如下几点不好的地方:
  • 1. 所引用的类型,必须通过ClassPool获取后才可以使用
  • 2. 代码块中所用到的引用类型,使用时必须写
  • 全量类名
  • 3. 即使代码块内容写错了,它也不会像eclipse等开发工具一样有提示,它只有在运行时才报错
  • 4. 动态修改的类,必须在修改之前,jvm中不存在这个类的实例对象。修改方法的实现必须在修改的类加载之前进行。
  • 参考:https://juejin.cn/post/6844903633100750862
参考:https://juejin.cn/post/6844903633100750862
posted on 2021-12-08 20:23  duanxz  阅读(3070)  评论(0编辑  收藏  举报