javassist介绍
(一)Javassist是什么
Javassist是可以动态编辑Java字节码的类库。它可以在Java程序运行时定义一个新的类,并加载到JVM中;还可以在JVM加载时修改一个类文件。Javassist使用户不必关心字节码相关的规范也是可以编辑类文件的。
使用流程:
(二)Javassist核心API
在Javassist中每个需要编辑的class都对应一个CtCLass实例,CtClass的含义是编译时的类(compile time class),这些类会存储在Class Pool中(Class pool是一个存储CtClass对象的容器)。 CtClass中的CtField和CtMethod分别对应Java中的字段和方法。通过CtClass对象即可对类新增字段和修改方法等操作了。
1. ClassPool:javassist的类池,使用ClassPool 类可以跟踪和控制所操作的类,它的工作方式与 JVM 类装载器非常相似,常见方法列表:
- getDefault : 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
- appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
- toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。
- 需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
- get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑。
2. CtClass: CtClass提供了类的操作,如在类中动态添加新字段、方法和构造函数、以及改变类、父类和接口的方法。,常见方法列表:
- freeze : 冻结一个类,使其不可修改;
- isFrozen : 判断一个类是否已被冻结;
- prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
- defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
- detach : 将该class从ClassPool中删除;
- writeFile : 根据CtClass生成 .class 文件;
- toClass : 通过类加载器加载该CtClass。
上面我们创建一个新的方法使用了CtMethod类。CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。
3. CtField:类的属性,通过它可以给类创建新的属性,还可以修改已有的属性的类型,访问修饰符等
4. CtMethod:类中的方法,通过它可以给类创建新的方法,还可以修改返回类型,访问修饰符等, 甚至还可以修改方法体内容代码。一些重要的方法:
- insertBefore : 在方法的起始位置插入代码;
- insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
- insertAt : 在指定的位置插入代码;
- setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
- 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方法。结果:
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); } }
结果:
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(); } }
结果:
(五)、总结
javassist被用于struts2和hibernate中,都用来做动态字节码修改使用。一般开发中不会用到,但在封装框架时比 较有用。虽然javassist提供了一套简单易用的API,但如果用于平常的开发,会有如下几点不好的地方:
- 1. 所引用的类型,必须通过ClassPool获取后才可以使用
- 2. 代码块中所用到的引用类型,使用时必须写
- 全量类名
- 3. 即使代码块内容写错了,它也不会像eclipse等开发工具一样有提示,它只有在运行时才报错
- 4. 动态修改的类,必须在修改之前,jvm中不存在这个类的实例对象。修改方法的实现必须在修改的类加载之前进行。
- 参考:https://juejin.cn/post/6844903633100750862
参考:https://juejin.cn/post/6844903633100750862