Javassist基本用法汇总

最近项目需要对基础架构做增强,需要基于字节码在不侵入原有代码的情况下实现, 故把javassist的基本用法过了一遍。这篇博客就是把主要讲讲为什么要用javassist以及javassist的基本用法。

1.为什么要使用javassist(上手成本低)

基于字节码增强的框架有两个ASM和javassit,下面是两个框架的特点以及对比

Javassist & ASM 对比

1.Javassist源代码级API比ASM中实际的字节码操作更容易使用
2.Javassist在复杂的字节码级操作上提供了更高级别的抽象层。Javassist源代码级API只需要很少的字节码知识,甚至不需要任何实际字节码知识,因此实现起来更容易、更快。
3.Javassist使用反射机制,这使得它比运行时使用Classworking技术的ASM慢。
总的来说ASM比Javassist快得多,并且提供了更好的性能。Javassist使用Java源代码的简化版本,然后将其编译成字节码。这使得Javassist非常容易使用,但是它也将字节码的使用限制在Javassist源代码的限制之内。
总之,如果有人需要更简单的方法来动态操作或创建Java类,那么应该使用Javassist API 。如果需要注重性能地方,应该使用ASM库。

2. javassist基本用法(基于3.28.0-GA的版本) 

Javassist 是一个开源的分析、编辑和创建Java字节码的类库. 其主要优点在于简单快速. 直接使用 java 编码的形式, 而不需要了解虚拟机指令, 就能动态改变类的结构, 或者动态生成类.

Javassist中最为重要的是ClassPool, CtClass, CtMethod以及CtField这几个类.

ClassPool: 一个基于Hashtable实现的CtClass对象容器, 其中键是类名称, 值是表示该类的CtClass对象
CtClass: CtClass表示类, 一个CtClass(编译时类)对象可以处理一个class文件, 这些CtClass对象可以从ClassPool获得
CtMethods: 表示类中的方法
CtFields: 表示类中的字段 

2.1 ClassPool对象

2.1.1 ClassPool的创建

// 获取ClassPool对象, 使用系统默认类路径
ClassPool pool = new ClassPool(true);
// 效果与 new ClassPool(true) 一致
ClassPool pool1 = ClassPool.getDefault();

为减少ClassPool可能导致的内存消耗. 可以从ClassPool中删除不必要的CtClass对象. 或者每次创建新的ClassPool对象.

// 从ClassPool中删除CtClass对象
ctClass.detach();
// 也可以每次创建一个新的ClassPool, 而不是ClassPool.getDefault(), 避免内存溢出
ClassPool pool2 = new ClassPool(true);

2.1.2 classpath

通过 ClassPool.getDefault()获取的ClassPool使用JVM的classpath.在Tomcat等Web服务器运行时, 服务器会使用多个类加载器作为系统类加载器, 这可能导致ClassPool可能无法找到用户的类. 这时, ClassPool须添加额外的classpath才能搜索到用户的类.

// 将classpath插入到指定classpath之前
pool.insertClassPath(new ClassClassPath(this.getClass()));
// 将classpath添加到指定classpath之后
pool.appendClassPath(new ClassClassPath(this.getClass()));
// 将一个目录作为classpath
pool.insertClassPath("/xxx/lib");

2.2 CtClass对象

2.2.1 获取CtClass

// 通过类名获取 CtClass, 未找到会抛出异常
CtClass ctClass = pool.get("com.kawa.ssist.JustRun");
// 通过类名获取 CtClass, 未找到返回 null, 不会抛出异常
CtClass ctClass1 = pool.getOrNull("com.kawa.ssist.JustRun"); 

2.2.2 创建CtClass

// 复制一个类
CtClass ctClass2 = pool.getAndRename("com.kawa.ssist.JustRun", "com.kawa.ssist.JustRunq");
// 创建一个新类
CtClass ctClass3 = pool.makeClass("com.kawa.ssist.JustRuna");
// 通过class文件创建一个新类
CtClass ctClass4 = pool.makeClass(new FileInputStream(new File("/home/un/test/JustRun.class")));

2.2.3 CtClass基础信息

// 类名
String simpleName = ctClass.getSimpleName();
// 类全名
String name = ctClass.getName();
// 包名
String packageName = ctClass.getPackageName();
// 接口
CtClass[] interfaces = ctClass.getInterfaces();
// 继承类
CtClass superclass = ctClass.getSuperclass();
// 获取类方法
CtMethod ctMethod = ctClass.getDeclaredMethod("getName()", new CtClass[] {pool.get(String.class.getName()), pool.get(String.class.getName())});
// 获取类字段
CtField ctField = ctClass.getField("name");
// 判断数组类型
ctClass.isArray();
// 判断原生类型
ctClass.isPrimitive();
// 判断接口类型
ctClass.isInterface();
// 判断枚举类型
ctClass.isEnum();
// 判断注解类型
ctClass.isAnnotation();
// 冻结一个类,使其不可修改
ctClass.freeze () 
// 判断一个类是否已被冻结
ctClass.isFrozen()
// 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用
ctClass.prune() 
//解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用prune方法
ctClass.defrost() 

2.2.4 CtClass类操作

// 添加接口
ctClass.addInterface(...);
// 添加构造器
ctClass.addConstructor(...);
// 添加字段
ctClass.addField(...);
// 添加方法
ctClass.addMethod(...);

2.2.5 CtClass类编译

// 获取字节码文件 需要注意的是一旦调用该方法,则无法继续修改已经被加载的class
Class clazz = ctClass.toClass();
// 类的字节码文件
ClassFile classFile = ctClass.getClassFile();
// 编译成字节码文件, 使用当前线程上下文类加载器加载类, 如果类已存在或者编译失败将抛出异常
byte[] bytes = ctClass.toBytecode();

2.3 CtMethod对象

2.3.1 获取CtMethod属性

CtClass ctClass5 = pool.get(TestService.class.getName());
CtMethod ctMethod = ctClass5.getDeclaredMethod("selectOrder");
// 方法名
String methodName = ctMethod.getName();
// 返回类型
CtClass returnType = ctMethod.getReturnType();
// 方法参数, 通过此种方式得到方法参数列表
// 格式: com.kawa.TestService.getOrder(java.lang.String,java.util.List)
ctMethod.getLongName();
// 方法签名 格式: (Ljava/lang/String;Ljava/util/List;Lcom/test/Order;)Ljava/lang/Integer;
ctMethod.getSignature();

// 获取方法参数名称, 可以通过这种方式得到方法真实参数名称
List<String> argKeys = new ArrayList<>();
MethodInfo methodInfo = ctMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
int len = ctMethod.getParameterTypes().length;
// 非静态的成员函数的第一个参数是this
int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;
for (int i = pos; i < len; i++) {
    argKeys.add(attr.variableName(i));
}

2.3.2 CtMethod方法体修改

// 在方法体前插入代码块
ctMethod.insertBefore("");
// 在方法体后插入代码块
ctMethod.insertAfter("");
// 在某行 字节码 后插入代码块
ctMethod.insertAt(10, "");
// 添加参数
ctMethod.addParameter(CtClass);
// 设置方法名
ctMethod.setName("newName");
// 设置方法体 $0=this / $1,$2,$3... 代表方法参数
ctMethod.setBody("{$0.name = $1;}");
//创建一个新的方法
ctMethod.make("kawa",CtClass);

2.3.3 异常块 addCatch()

在方法中加入try catch块, 需要注意的是, 必须在插入的代码中, 加入return值$e代表异常信息.插入的代码片段必须以throw或return语句结束

CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.io.IOException");
m.addCatch("{ System.out.println($e); throw $e; }", etype);
// 等同于添加如下代码: 
try {
    // the original method body
} catch (java.io.IOException e) {
    System.out.println(e);
    throw e;
}

2.4 特殊标识

$0

方法调用的目标对象. 它不等于this, 它代表了调用者. 如果方法是静态的, 则$0为null.

$1, $2 ..

方法的参数 m.insertBefore("{ System.out.println($1); System.out.println($2); }");

$$

是所有方法参数的简写, 主要用在方法调用上. 例如:

// 原方法
move(String a,String b)
move($$)
move($1,$2)
// 如果新增一个方法, 方法含有move的所有参数, 则可以这些写: 
move($$, context)
move($1, $2, context)

$args

$args 指的是方法所有参数的数组,类似Object[],如果参数中含有基本类型,则会转成其包装类型。需要注意的时候,$args[0]对应的是$1,而不是$0,$0!=$args[0],$0=this

$cflow

$cflow意思为控制流(control flow),是一个只读的变量,值为一个方法调用的深度。例

//原方法
int fact(int n) {
    if (n <= 1)
        return n;
    else
        return n * fact(n - 1);
}
//javassist调用
CtMethod cm = ...;
//这里代表使用了cflow
cm.useCflow("fact");
//这里用了cflow,说明当深度为0的时候,就是开始当第一次调用fact的方法的时候,打印方法的第一个参数
cm.insertBefore("if ($cflow(fact) == 0)"
              + "    System.out.println(\"fact \" + $1);");

$_ 与 $r

$_是方法调用的结果;$r是返回结果的类型, 用于强制类型转换

Object result = ... ;
$_ = ($r)result; 

$w

基本类型的包装类 Integer i = ($w)5;

$class

一个 java.lang.Class 对象, 表示当前正在修改的类

$sig

类型为 java.lang.Class 的参数类型数组

$type

一个 java.lang.Class 对象, 表示返回值类型

$proceed

调用表达式中方法的名称

 

参考博客:https://zhuanlan.zhihu.com/p/349661837

参考文档:https://www.javassist.org/tutorial/tutorial.html 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章