Javassist | 字节码增强技术
Javassist 是一个开源的分析、编辑和创建Java字节码的类库. 其主要优点在于简单快速. 直接使用 java 编码的形式, 而不需要了解虚拟机指令, 就能动态改变类的结构, 或者动态生成类.
一. Javassist的重要组成
Javassist中最为重要的是ClassPool, CtClass, CtMethod以及CtField这几个类.
ClassPool: 一个基于Hashtable实现的CtClass对象容器, 其中键是类名称, 值是表示该类的CtClass 对象.
CtClass: CtClass表示类, 一个CtClass(编译时类)对象可以处理一个class文件, 这些CtClass对象可以从ClassPool获得.
CtMethods: 表示类中的方法.
CtFields: 表示类中的字段.
二. Hello world
2.1
原始类
例如, 我们有如下类:
package com.in.aop.javassist;
public class TestObject {
public static String getValue(String key) {
return key;
}
}
2.2
增强
如何在不修改代码的前提下, 增加一段简单逻辑: 在返回结果前, 将参数打印出来?
public static void main(String[] args) throws NotFoundException, CannotCompileException {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.in.aop.javassist.TestObject");
CtMethod m = cc.getDeclaredMethod("getValue", new CtClass[]{pool.get(String.class.getName())});
m.insertBefore("System.out.println(\"hello:\"+ $1 );");
cc.toClass();
System.out.println(TestObject.getValue("world"));
}
2.3
执行结果
hello:world
world
2.4
pom依赖
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>
2.5
过程简析
1. 先在池中, 找到我们要黑掉的类和方法.
2. 在方法体中插入增强语句
整个流程虽然非常简单, 但合理利用javassist API会完成更多功能.
三. 常用API
3.1
ClassPool对象
// 获取ClassPool对象, 使用系统默认类路径
ClassPool pool = new ClassPool(true);
// 效果与 new ClassPool(true) 一致
ClassPool pool1 = ClassPool.getDefault();
3.2
类对象
3.2.1
获取类对象
// 通过类名获取 CtClass, 未找到会抛出异常
CtClass ctClass = pool.get("test.DemoService");
// 通过类名获取 CtClass, 未找到返回 null, 不会抛出异常
CtClass ctClass1 = pool.getOrNull("test.DemoService");
3.2.2
创建新类
// 复制一个类
CtClass ctClass2 = pool.getAndRename("test.DemoService", "test.CopyService");
// 创建一个新类
CtClass ctClass3 = pool.makeClass("test.NewDemoService");
// 通过class文件创建一个新类
CtClass ctClass4 = pool.makeClass(new FileInputStream(new File("./xxx/Demo.class")));
3.3
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");
3.4
内存回收
为减少ClassPool可能导致的内存消耗. 可以从ClassPool中删除不必要的CtClass对象. 或者每次创建新的ClassPool对象.
// 从ClassPool中删除CtClass对象
ctClass.detach();
// 也可以每次创建一个新的ClassPool, 而不是ClassPool.getDefault(), 避免内存溢出
ClassPool pool2 = new ClassPool(true);
3.5
类信息
3.5.1
基础信息
// 类名
String simpleName = ctClass.getSimpleName();
// 类全名
String name = ctClass.getName();
// 包名
String packageName = ctClass.getPackageName();
// 接口
CtClass[] interfaces = ctClass.getInterfaces();
// 继承类
CtClass superclass = ctClass.getSuperclass();
// 获取类方法
CtMethod ctMethod = ctClass.getDeclaredMethod("getXxx()", new CtClass[] {pool.get(String.class.getName()), pool.get(String.class.getName())});
// 获取类字段
CtField ctField = ctClass.getField("xxx");
// 判断数组类型
ctClass.isArray();
// 判断原生类型
ctClass.isPrimitive();
// 判断接口类型
ctClass.isInterface();
// 判断枚举类型
ctClass.isEnum();
// 判断注解类型
ctClass.isAnnotation();
3.5.2
类操作
// 添加接口
ctClass.addInterface(...);
// 添加构造器
ctClass.addConstructor(...);
// 添加字段
ctClass.addField(...);
// 添加方法
ctClass.addMethod(...);
3.5.3
类编译
// 获取字节码文件
Class clazz = ctClass.toClass();
// 类的字节码文件
ClassFile classFile = ctClass.getClassFile();
// 编译成字节码文件, 使用当前线程上下文类加载器加载类, 如果类已存在或者编译失败将抛出异常
byte[] bytes = ctClass.toBytecode();
3.6
方法信息
3.6.1
获取方法属性
CtClass ctClass5 = pool.get(TestService.class.getName());
CtMethod ctMethod = ctClass5.getDeclaredMethod("selectOrder");
// 方法名
String methodName = ctMethod.getName();
// 返回类型
CtClass returnType = ctMethod.getReturnType();
// 方法参数, 通过此种方式得到方法参数列表
// 格式: test.TestService.getXX(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));
}
3.6.2
方法体修改
// 在方法体前插入代码块
ctMethod.insertBefore("");
// 在方法体后插入代码块
ctMethod.insertAfter("");
// 在某行 字节码 后插入代码块
ctMethod.insertAt(10, "");
// 添加参数
ctMethod.addParameter(CtClass);
// 设置方法名
ctMethod.setName("newName");
// 设置方法体
ctMethod.setBody("");
3.6.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;
}
3.7
特殊标识
$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)
$_ 与 $r
$_是方法调用的结果;$r是返回结果的类型, 用于强制类型转换
Object result = ... ;
$_ = ($r)result;
$w
基本类型的包装类
Integer i = ($w)5;
$class
一个 java.lang.Class 对象, 表示当前正在修改的类
$sig
类型为 java.lang.Class 的参数类型数组
$type
一个 java.lang.Class 对象, 表示返回值类型
$class
一个 java.lang.Class 对象, 表示当前正在修改的类
$proceed
调用表达式中方法的名称
小结
Javassist虽然性能上比ASM,bcel较差, 但不需要了解字节码命令, 对开发者要求也不那么高, 开发效率也很高.
同时, 配合javaAgent和instrument技术, 动态插入代码, 无侵入实现埋点, 获取方法执行时间, 热部署以及流量回放等功能.
推 / 荐 / 阅/ 读
●
●
●