Javassist概述
Javassist
是可以动态编辑Java字节码的类库。它可以在Java程序运行时定义一个新的类,并加载到JVM中;还可以在JVM加载时修改一个类文件,添加新的方法,或者是修改已有的方法。Javassist使用户不必关心字节码相关的规范也是可以编辑类文件的。
Java常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是Javassit。
核心API
Javassist中每个需要编辑的class都对应一个CtCLass
实例,CtClass的含义是编译时的类(compile time class),这些类会存储在ClassPool
中(ClassPool是一个存储CtClass对象的容器)。
CtClass中的CtField
和CtMethod
分别对应Java中的字段和方法。通过CtClass对象即可对类新增字段和修改方法等操作了。

ClassPool
ClassPool是 CtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。
getDefault
:返回默认ClassPath 下的 ClassPool是 单例模式,一般通过该方法创建我们的ClassPool;appendClassPath
,insertClassPath
:将一个 ClassPath 加到类搜索路径的末尾位置或插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬。toClass
:将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class。get
,getCtClass
:根据类路径名获取该类的CtClass对象。makeClass
:创建新类。makeInterface
:创建新接口。接口中的成员方法可以在 CtNewMethod 中使用 abstractMethod () 创建。
需要注意的是 ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,API中给出的解决方案是 有意识的调用 CtClass 的 detach()
方法以释放内存。
public class TestBean2 {
public static void main(String[] args) {
//获取默认ClassPath 下的 ClassPool
ClassPool pool = ClassPool.getDefault();
//插入ClassPath加到类搜索路径
ClassLoader cl = Thread.currentThread().getContextClassLoader();
pool.insertClassPath(new LoaderClassPath(cl));
try {
//获取指定类CtClass
CtClass ctClass = pool.get("com.ymqx.动态增加属性和注解.TargetBean");
} catch (NotFoundException e) {
e.printStackTrace();
}
//创建新类
CtClass newCtClass = pool.makeClass("com.ymqx.动态增加属性和注解.DestBean");
//将修改后的CtClass加载至当前线程的上下文类加载器中
try {
pool.toClass(newCtClass);
} catch (CannotCompileException e) {
e.printStackTrace();
}
}
}
静态方法 ClassPool.getDefault () 返回的默认 ClassPool 将搜索底层 JVM (Java 虚拟机) 具有的同一路径。如果某个程序在 web 应用程序服务器 (如 JBoss 和 Tomcat) 上运行, 则 ClassPool 对象可能无法找到用户类, 因为这样的 web 应用程序服务器使用多个类加载器以及系统类加载程序。在这种情况下, 必须将附加的类路径注册到 ClassPool。
CtClass
CtClass 是类文件的抽象表示形式,CtClass 对象是处理类文件的句柄。
isFrozen
:判断一个类是否已被冻结。isModified
:如果类被修改过的话就返回true,否则返回false。isPrimitive
:是否为Java原始类型:boolean, byte, char, short, int, long, float, double, or void。prune
:删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用。freeze
:冻结一个类,使其不可修改。defrost
:解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法。detach
: 将该class从ClassPool中删除。getName
:获取CtClass代表的类的全限定类名。setName
:修改类名,必须以全限定类名的形式传递参数。isInterface
、isAnnotation
、isEnum
:CtClass代表的类对象是否为接口、注解、枚举类型。getField
、getMethod
、getConstructor
:获取类的字段、方法、构造函数。getFields
、getMethods
、getConstructors
:获取类的字段、方法、构造函数的集合。addField
、addMethod
、addConstructor
:类中添加字段、方法、构造函数。removeField
、removeMethod
、removeConstructor
:从类中移除字段、方法、构造函数。insturment
:向类中注册CodeConverter或者ExprEditor,其中CodeConverter用于代码的转换,ExprEditor用于编辑方法体中满足条件的代码块,instrument(ExprEditor)是CtMethod和CtConstructor调用的方法。toClass
、toBytecode
、writeFile
:将CtClass对象转换为Class对象、bytecode字节码、写入.class文件中,调用这些方法后,CtClass对象将会被冻结,无法进行修改。- setSuperclass:设置父类。
CtField
CtField代表类的字段,既可以通过CtClass获取类中已经存在的字段,也可以新建一个CtField对象并添加到CtClass中。
make(String src, CtClass declaring)
:通过源码的形式构造CtField对象,例如src可以为:“public String name;”,不要忘记";",否则会编译失败,declaring代表我们要添加字段的类getName
:获取字段名setName
:修改字段名getModifiers
、setModifiers
:获取访问修饰符、设置访问修饰符,其中,设置访问修饰符可以通过Modifier中提供的常量字段设置,并且可以组合设置,例如要设置public static,可以使用如下设置:Modifier.PUBLIC | Modifier.STATICgetAnnotation
、getAnnotations
:获取字段上的注解getSignature
:返回代表字段类型的字符串,如果是String类型的话,返回结果为:Ljava/lang/String;getGenericSignature
、setGenericSignature
:获取和设置字段的泛型类型getConstantValue
:只能获取Java基本类型的包装类型对应的常量值,如果是其他类型的常量的话则会返回nullgetFieldInfo
:获取字段信息,用于获取、修改、新增注解
在向CtClass类中添加新字段的时候,可以添加字段的初始值,在调用CtClass.addField方法时,向其中传入CtField.Initializer参数即可设置字段对应的初始值,例如:
CtField username = new CtField(pool.get("java.lang.String"), "username", ctClass);
username.setModifiers(Modifier.PRIVATE);
ctClass.addField(username, CtField.Initializer.constant("ZhangSan"));
示例1:创建新类,添加一个int类型的,名字为value的变量,以及getter/setter
public class TestBean2 {
public static void main(String[] args) {
//获取默认ClassPath 下的 ClassPool
ClassPool pool = ClassPool.getDefault();
// 创建一个新类
CtClass ctClass = pool.makeClass("com.ymqx.动态增加属性和注解.CreateBean");
//添加一个int类型的,名字为value的变量,以及getter/setter
try {
CtField ctField = new CtField(CtClass.intType,"value",ctClass);
ctField.setModifiers(Modifier.PRIVATE);
ctClass.addField(ctField);
//使用CtNewMethod添加getter/setter
CtMethod getter = CtNewMethod.getter("getValue", ctField);
CtMethod setter = CtNewMethod.setter("setValue", ctField);
ctClass.addMethod(getter);
ctClass.addMethod(setter);
} catch (CannotCompileException e) {
e.printStackTrace();
}
//生成字节码文件,方便查看创建出来的类的结果
try {
ctClass.writeFile(System.getProperty("user.dir") + "\\target\\classes");
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行会生成新的.class文件
package com.ymqx.动态增加属性和注解;
public class CreateBean {
private int value;
public int getValue() {
return this.value;
}
public void setValue(int var1) {
this.value = var1;
}
public CreateBean() {
}
}
CtConstructor
CtConstructor代表类的构造函数,也可能代表类的初始化函数;可以通过isClassInitializer()方法确定是否为类初始化函数。
getLongName
:获取带有参数的名称(全限定类名和构造函数参数类型),如:javassist.CtConstructor(CtClass[],CtClass)getName
:只获取构造函数名也就是类名isConstructor
:是否为构造函数isClassInitializer
:是否为类的初始化函数callSuper
:是否调用父类构造函数setBody
:设置构造函数源码insertBeforeBody
:在构造函数开头插入代码setModifiers
:设置访问权限
通过上述方法,我们可以自定义构造函数并添加到CtClass对象中去,也可以访问现有构造函数的相关信息。如果需要创建CtConstructor对象,可以通过 CtNewConstructor
make
:构造CtNewConsturctor对象copy
:利用原有的CtConstructor对象复制一个新的CtConstructor对象defaultConstructor
:创建默认构造函数skeleton
:创建一个带有参数的构造函数,但是该函数体只调用super()方法,其余代码需要自己编写插入
// 方法一,通过new CtConstructor()的方式创建CtConstructor对象
// new CtClass[]{}相当于构造函数的参数,ctClass为要添加构造函数的类对象
CtConstructor none = new CtConstructor(new CtClass[] {}, ctClass);
none.setBody("System.out.println(\"使用javassist产生的默认构造函数\");");
ctClass.addConstructor(none);
// 方法二,通过CtNewConstructor提供的静态方法创建CtConstructor对象
CtNewConstructor.make("public User() {System.out.println(\"使用javassist产生的默认构造函数\");}", ctClass);
CtMethod
CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。
make
:通过源码的形式创建CtMethod对象getName
:获取函数名setName
:设置函数名(可以用于修改函数名称)setBody
:设置函数体setWrappedBody
:通过提供的CtMethod对象的函数体来设置当前CtMethod对象的函数体setModifiers
:设置函数的访问修饰符insertBefore
、insertAfter
、insertAt
:在函数最开始 | 结束位置 | 任意位置(需要知道代码行号)插入代码addLocalVariable
:添加局部变量addCatch
:添加try catch语句
编译的时候,会把变量名抹掉,传递的参数会依次在局部变量表中的顺序。
比如,如果方法体是 public void test(int a,int b,int c){ … } ,那么a,b,c就对应本地变量表中的1,2,3的位置。Javassist中获取变量时就不能使用原始的名字,参数使用的是$1,$2,$3。
还可以通过 CtNewMethod生成方法
make
:通过源码构造CtMethod对象copy
:利用原有的CtMethod对象复制一个新的CtMethod对象abstractMethod
:接口方法只能通过CtNewMethod.abstractMethod来创建抽象方法getter
:生成getter方法setter
:生成setter方法
示例2:创建新类,添加一个hello的方法
public class TestBean2 {
public static void main(String[] args) {
//获取默认ClassPath 下的 ClassPool
ClassPool pool = ClassPool.getDefault();
// 创建一个新类
CtClass ctClass = pool.makeClass("com.ymqx.动态增加属性和注解.CreateBean");
//添加一个hello的方法
try {
CtMethod ctMethod = new CtMethod(CtClass.intType, "hello", new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("return $1 + $2;");
//在方法体的前后分别插入代码
ctMethod.insertBefore("System.out.println(\"在前面插入了:\" + $1);");
ctMethod.insertAfter("System.out.println(\"在最后插入了:\" + $1);");
ctClass.addMethod(ctMethod);
} catch (CannotCompileException e) {
e.printStackTrace();
}
//生成字节码文件,方便查看创建出来的类的结果
try {
ctClass.writeFile(System.getProperty("user.dir") + "\\target\\classes");
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行会生成新的.class文件
package com.ymqx.动态增加属性和注解;
public class CreateBean {
public int hello(int var1, double var2) {
System.out.println("在前面插入了:" + var1);
double var5 = (double)var1 + var2;
System.out.println("在最后插入了:" + var1);
return (int)var5;
}
public CreateBean() {
}
}
示例3:创建新类,使用CtNewMethod添加方法
public class TestBean2 {
public static void main(String[] args) {
//获取默认ClassPath 下的 ClassPool
ClassPool pool = ClassPool.getDefault();
// 创建一个新类
CtClass ctClass = pool.makeClass("com.ymqx.动态增加属性和注解.CreateBean");
try {
//添加一个int类型的,名字为value的变量,以及getter/setter
CtField ctField = new CtField(CtClass.intType,"value",ctClass);
ctField.setModifiers(Modifier.PRIVATE);
ctClass.addField(ctField);
//使用CtNewMethod添加getter/setter
CtMethod getter = CtNewMethod.getter("getValue", ctField);
CtMethod setter = CtNewMethod.setter("setValue", ctField);
ctClass.addMethod(getter);
ctClass.addMethod(setter);
//添加一个hello2的方法
CtMethod ctMethod = CtNewMethod.make("public int hello2(int num1, double num2) {\n" +
" double sum = (double)num1 + num2;\n" +
" return (int)sum;\n" +
"}", ctClass);
ctClass.addMethod(ctMethod);
} catch (CannotCompileException e) {
e.printStackTrace();
}
//生成字节码文件,方便查看创建出来的类的结果
try {
ctClass.writeFile(System.getProperty("user.dir") + "\\target\\classes");
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
生成的 .class文件:
package com.ymqx.动态增加属性和注解;
public class CreateBean {
private int value;
public int getValue() {
return this.value;
}
public void setValue(int var1) {
this.value = var1;
}
public int hello2(int var1, double var2) {
double var4 = (double)var1 + var2;
return (int)var4;
}
public CreateBean() {
}
}
CtNewMethod.getter()和CtNewMethod.setter()更简洁生成getter/setter方法,make()方法生成的方法,变量名都会抹掉,替换成var1、var2等
Javassist中特殊参数示例
标识符 | 作用 |
---|---|
$0 | this |
$1、$2、 … | 方法参数(1-N是方法参数的顺序) |
$args | 方法参数数组,类型为Object[] |
$$ | 所有方法参数,例如:m($$)相当于m($1,$2,…) |
$cflow(…) | control flow,这是一个只读变量,返回指定方法递归调用的深度。 |
$r | 返回结果的类型,在强制转换表达式中使用。 |
$w | 包装器类型,在强制转换表达式中使用。 |
$_ | 返回的结果值 |
$sig | 类型为java.lang.Class的参数类型对象数组 |
$type | 类型为java.lang.Class的返回值类型 |
$class | 类型为java.lang.Class的正在修改的类 |
目前只用到简单方法,具体用法参考:
关于Java字节码编程javassist的详细介绍
ConstPool
创建属性相关实例时,比如,给字段属性增加注解等,都需要参数ConstPool常量池。
获取方式:
ClassPool pool = ClassPool.getDefault();
//获取实体类
CtClass ctClass = pool.get("类名");
ConstPool constPool = ctClass.getClassFile().getConstPool();