上一篇我们简单介绍了ASM,这一篇我们介绍一下Javassist。javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的Shigeru Chiba (千叶滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类(这一点与ASM相比用起来爽很多,但是性能上还是ASM更快)。
Javassist中最为重要的是ClassPool,CtClass ,CtMethod 以及 CtField这几个类。
ClassPool: 一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。需要的注意的是当CtClass数量过多时,会占用大量的内存,API中给出的解方案是有意识的调用CtClass的detach方法释放内存。
- getDefault : 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
- appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
- toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
- get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑。
CtClass: 表示一个类,这些CtClass对象可以从ClassPool获得。
CtMethods: 表示类中的方法。
- insertBefore : 在方法的起始位置插入代码;
- insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
- insertAt : 在指定的位置插入代码;
- setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
- make : 创建一个新的方法。
需要注意的是 传入insertBefore(),insertAfter(),addCatch()和insertAt()的字符串对象被Javassist包含的编译器编译。因此编译器支持语言扩展,专有的以$开头的标识符有特殊的意义:
$0, $1, $2,… | 参数数组,$args的类型为object[] |
---|---|
$args | this和实际的参数 |
$$ | 所有实际的参数,例如m($$)与m($1,$2,…)等同 |
$cflow(…) | cflow变量 |
$r | 返回类型。它被用在转换表达式中。 |
$w | 包装类类型。它被用在转换表达式中。 |
$_ | 返回值 |
$sig | 一个java.lang.Class对象数组,表示正式的参数类型。 |
$type | 一个java.lang.Class对象,表示了正式的返回类型。 |
$class | 一个java.lang.Class对象,表示当前编辑的类。 |
$0, $1, $2, …
传入目标方法的参数需要使用$1, $2, … 而不是原始的参数名。$1 表示第一个参数,$2 表示第二个参数等等。这些变量的类型和参数的类型一致。$0 等于this。如果方法是静态的,$0 无法获取。
另外需要注意的是:上面的insertBefore() 和 setBody()中的语句,如果你是单行语句可以直接用双引号,但是有多行语句的情况下,你需要将多行语句用{}括起来。javassist只接受单个语句或用大括号括起来的语句块。
更多详情参考javassist文档.
CtFields :表示类中的字段。
动态生成一个类:
import javassist.*;
import javassist.bytecode.AccessFlag;
import java.io.IOException;
import java.lang.reflect.Field;
public class Test {
public static void genClassMethod() {
ClassPool pool = ClassPool.getDefault();
// 创建一个public类,假如类已经存在会覆盖原类
CtClass ct = pool.makeClass("com.pch.javassist.GenClass");
// 让类实现Cloneable接口
ct.setInterfaces(new CtClass[]{pool.makeInterface("java.lang.Cloneable")});
try {
// 获得一个类型为int,名称为id的字段
CtField f = new CtField(CtClass.intType, "id", ct);
// 将字段设置为public
f.setModifiers(AccessFlag.PUBLIC);
// 将字段设置到类上
ct.addField(f);
// 添加构造函数
CtConstructor constructor = CtNewConstructor.make("public GenClass(int id) {this.id = id;}", ct);
ct.addConstructor(constructor);
// 添加方法
CtMethod testM = CtNewMethod.make("public void print(String des){ System.out.println(des);}", ct);
ct.addMethod(testM);
// 将生成的.class文件保存到磁盘
ct.writeFile();
// 下面的代码为验证代码
Field[] fields = ct.toClass().getFields();
System.out.println("属性名称:" + fields[0].getName() + " 属性类型:" + fields[0].getType());
} catch (CannotCompileException | NotFoundException | IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
genClassMethod();
}
}
这样我们就动态的生成了一个类。是不是比ASM使用起来简单很多。
注意:使用过程中如果出现以下报错
Exception in thread "main" java.lang.NullPointerException
at javassist.util.proxy.SecurityActions.setAccessible(SecurityActions.java:103)
at javassist.util.proxy.DefineClassHelper.toClass3(DefineClassHelper.java:151)
at javassist.util.proxy.DefineClassHelper.toClass2(DefineClassHelper.java:134)
at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:95)
at javassist.ClassPool.toClass(ClassPool.java:1143)
at javassist.ClassPool.toClass(ClassPool.java:1106)
at javassist.ClassPool.toClass(ClassPool.java:1064)
at javassist.CtClass.toClass(CtClass.java:1275)
at com.pch.javassist.Test.genClassMethod(Test.java:34)
at com.pch.javassist.Test.main(Test.java:42)
- 首先,检查是不是路径有问题,如果你使用的是idea,看一下Working directory的路径
- 如果路径没有问题,需要看一下JDK版本与javassist版本是否一致?
例如:我们本地使用的是jdk11,而我们用的是javassist的3.22.0-GA版本就不行,查看ClassPool源码我们发现
public class ClassPool {
private static java.lang.reflect.Method definePackage = null;
static {
// 原因原来在这
if (ClassFile.MAJOR_VERSION < ClassFile.JAVA_9)
try {
AccessController.doPrivileged(new PrivilegedExceptionAction(){
public Object run() throws Exception{
Class cl = Class.forName("java.lang.ClassLoader");
definePackage = cl.getDeclaredMethod("definePackage",
new Class[] { String.class, String.class, String.class,
String.class, String.class, String.class,
String.class, java.net.URL.class });
return null;
}
});
}
catch (PrivilegedActionException pae) {
throw new RuntimeException("cannot initialize ClassPool",
pae.getException());
}
}
所以,我们要保证javassist的版本与jdk版本匹配。
其实,javassist提供了手动设置路径的方式
ClassPool pool = ClassPool.getDefault();
// 设置类路径
pool.appendClassPath("***/Fighting/src/main/java/");
// 创建一个public类,假如类已经存在会覆盖原类
CtClass ct = pool.makeClass("com.pch.javassist.GenClass");
同时,写入方法也支持路径指定
//这里会将这个创建的类对象编译为.class文件
cc.writeFile("**/Fighting/src/main/java/");
动态修改方法体
AOP就是使用这种技术,动态的在一个方法中插入代码,下面我们举一个例子
例如,我们有一个如下类
public class Point {
private int x;
private int y;
public Point() {
}
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public void move(int dx, int dy) {
this.x += dx;
this.y += dy;
}
}
动态的在move方法体前后插入一些代码
private static void modifyMethod() {
ClassPool pool = ClassPool.getDefault();
try {
CtClass ct = pool.getCtClass("com.pch.javassist.Point");
CtMethod m = ct.getDeclaredMethod("move");
m.insertBefore("{ System.out.print(\"dx:\"+$1); System.out.println(\"dy:\"+$2);}");
m.insertAfter("{System.out.println(this.x); System.out.println(this.y);}");
ct.writeFile();
//通过反射调用方法,查看结果
Class pc = ct.toClass();
Method move = pc.getMethod("move", new Class[]{int.class, int.class});
Constructor<?> con = pc.getConstructor(new Class[]{int.class, int.class});
move.invoke(con.newInstance(1, 2), 1, 2);
} catch (Exception e) {
e.printStackTrace();
}
}
另外需要注意的是:上面的insertBefore() 和 setBody()中的语句,如果你是单行语句可以直接用双引号,但是有多行语句的情况下,你需要将多行语句用{}括起来。javassist只接受单个语句或用大括号括起来的语句块。
需要注意的是: 语句和代码块可以引用属性和方法。如果方法是用-g选项编译的(也可以在类文件中包含局部变量属性),它们也可以引用它们插入的方法的参数。否则,它们必须通过下文描述的特殊变量$0, $1, $2等访问方法的参数。访问方法声明的本地变量是不允许的,尽管允许代码块声明新的本地变量。然而,如果这些变量在指定的行号可以获得且目标方法使用-g选项编译,insertAt()运行语句和代码块访问本地变量。
Javassist不允许移除方法或属性,但是它允许修改其名称。Javassist也不允许添加一个额外的参数到现有的方法。反之,应该添加一个新的接受额外参数或者其它参数的方法。
javassist先介绍到这里,下一篇文章我们介绍一下APT框架
参考:
- https://www.cnblogs.com/rickiyang/p/11336268.html
- https://blog.csdn.net/ShuSheng0007/article/details/81269295
- https://www.jianshu.com/p/b0986a86cd62