基于javassist处理java字节码(一)
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字节码的。这些类名的前缀Ct
是compile 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\");");