字节码增强之Javassist
Posted 风在哪
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了字节码增强之Javassist相关的知识,希望对你有一定的参考价值。
字节码增强之Javassist
Javassist(Java Programming Assist)是编辑字节码的Java类库,它使Java字节码操作变得简单。通过使用Javassist可以使Java程序在运行时定义一个新的类,并且在JVM加载类文件时修改它。Javassist提供两个级别的API:源码级别和字节码级别。如果使用源码级别的API,我们可以在不知道Java字节码知识的情况下编辑Java类文件,就像我们编写Java源代码一样方便。如果使用字节码级别的API,那么需要我们详细了解Java字节码和类文件格式,因为字节码级别的API允许我们对类文件进行任意修改。
Javassist官方教程:Javassist Tutorial
本篇博客结合官方教程讲解了Javassist的几种用法,以及源码级别API的几个重要的工具类中包含的方法
1. start
要使用Javassist,我们需要引入对应的依赖,如果是maven项目的话,我们直接引用下面的代码即可:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
从一个简单的例子开始:生成一个新类
首先让我们回忆下我们自己如何编写一个Java类:
- 确定类名
- 添加字段
- 添加构造函数
- 添加成员函数
那么使用Javassist生成一个新类也包含类似的步骤:
- 获取ClassPool对象(ClassPool代表CtClass的容器)
- 通过ClassPool对象构建一个CtClass对象(一个CtClass代表一个Class的容器)(确定类名)
- 通过CtField对象向类中添加类字段(添加字段)
- 通过CtConstructor对象向类中添加构造函数(添加构造函数)
- 通过CtMethod对象向类中添加方法(添加成员函数)
其实使用Javassist生成一个新类和我们自己编写一个类的过程差不多,只是Javassist是通过API来完成了我们通过源码完成的事。
接下来看看如何使用Javassist创建一个新类吧:
/*
使用Javassist生成一个新的类
*/
public static void createUser() throws Exception
ClassPool pool = ClassPool.getDefault();
// 构造一个CtClass对象
CtClass ctClass = pool.makeClass("cn.wygandwdn.javassist.learn.User");
// 添加属性字段
CtField username = new CtField(pool.get("java.lang.String"), "username", ctClass);
username.setModifiers(Modifier.PRIVATE);
ctClass.addField(username, CtField.Initializer.constant("ZhangSan"));
CtField pass = new CtField(pool.get("java.lang.String"), "password", ctClass);
pass.setModifiers(Modifier.PRIVATE);
ctClass.addField(pass, CtField.Initializer.constant("123456"));
// 添加构造函数
// 默认构造函数
CtConstructor none = new CtConstructor(new CtClass[] , ctClass);
none.setBody("System.out.println(\\"使用javassist产生的默认构造函数\\");");
ctClass.addConstructor(none);
// 构造函数
CtConstructor constructor = new CtConstructor(new CtClass[] pool.get("java.lang.String"), pool.get("java.lang.String"),
ctClass);
constructor.setBody("$0.username = $1;$0.password = $2;");
ctClass.addConstructor(constructor);
// 添加getter和setter方法
CtMethod setUsername = new CtMethod(CtClass.voidType, "setUsername", new CtClass[] pool.get("java.lang.String"),
ctClass);
setUsername.setModifiers(Modifier.PUBLIC);
setUsername.setBody("$0.username = $1;");
ctClass.addMethod(setUsername);
CtMethod getUsername = new CtMethod(pool.get("java.lang.String"), "getUsername", new CtClass[] , ctClass);
getUsername.setModifiers(Modifier.PUBLIC);
getUsername.setBody("return $0.username;");
ctClass.addMethod(getUsername);
CtMethod setPass = CtNewMethod.setter("setPass", pass);
ctClass.addMethod(setPass);
CtMethod getPass = CtNewMethod.getter("getPass", pass);
ctClass.addMethod(getPass);
Class<?> user = ctClass.toClass();
Object instance = user.getDeclaredConstructors()[0].newInstance();
System.out.println(instance);
在上面的例子中,我们用到了ClassPool、CtClass、CtField、CtConstructor、CtMethod等工具类,接下来就看看这些工具类的用处吧
2. middle
本小节将介绍Javassist中常用的一些源码级别的API:
- ClassPool:存放CtClass的容器
- CtClass:代表一个class or interface or annotation
- CtField:代表class中的某个字段
- CtConstructor:代表class的构造函数
- CtMethod:代表class的包含的方法
2.1 ClassPool
ClassPool是存放CtClass的容器,CtClass只能通过ClassPool提供的方法获取,其中包括get()&makeClass()方法;ClassPool通过HashTable来缓存已经创建好的CtClass对象,其中键为className,值为CtClass对象
下面是一些常用的方法:
- getDefault():获取ClassPool的单例对象,并且添加SystemClassPath
- get():根据全限定类名获取对应的CtClass对象,如果ClassPool中没有缓存对应的CtClass对象则新建一个CtClass对象缓存并返回
- getAndRename():根据全限定类名获取CtClass对象,并且重命名类名
- find():根据全限定类名获取资源位置
- makeClass():根据全限定类名创造CtClass对象并返回
- appendClassPath&insertClassPath:向类搜索路径列表尾部&头部插入ClassPath
- toClass():将CtClass对象转换为Class对象
- importPackage():将Java包导入到ClassPool中,方便其通过package查找到对应的类
ClassPool中如果缓存大量的CtClass对象,可能会消耗大量缓存;为了避免这种情况的发生,我们可以调用CtClass.detach()方法,该方法将会删除ClassPool中相应的CtClass对象,释放空间;如果我们调用get()方法获取被删除的CtClass对象,那么ClassPool会重新生成相应的CtClass对象
ClassPool可以构造类似于Java类加载器机制的级联ClassPool,当调用get()方法时,先委托给父ClassPool尝试获取CtClass对象,如果父类ClassPool无法获取到CtClass对象则子ClassPool会尝试加载类文件(只存在父子ClassPool的情况下),我们可以通过ClassPool.get()方法发现这个机制:
上述代码中parent就相当于父类ClassPool,如果childFirstLookup为false并且parent非空,则会委托给父ClassPool检索CtClass对象
实现该机制非常简单:
public static void parentClassPool() throws NotFoundException
ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.appendClassPath("D:/java_project/trace/learn-javassist/src/main/java");
通过向ClassPool的构造函数传入父ClassPool即可
2.2 CtClass
CtClass实例代表一个Java类,只能通过ClassPool获取它,我们可以通过ClassPool的get()&makeClass()方法来获取CtClass对象
CtClass是一个抽象类,并且它的绝大部分方法都是返回null或者抛出异常;这些方法会被其子类重写。CtClass的子类包括:CtClassType、CtPrimitiveType、CtArray
当我们获取到CtClass对象之后,就可以对其进行修改,可以修改、添加、删除类的字段和方法;其中对于方法的修改可以直接修改整个方法体,或者修改方法体中的某个表达式(也就是某行语句)
首先看看CtClass一些常用的方法:
- isModified():如果类被修改过的话就返回true,否则返回false
- isFrozen():如果类被冻结则返回true,否则返回false;类是否被冻结表明类是否能够修改,如果类被冻结了则不能进行修改,否则可以修改
- freeze():冻结类,使其不能被修改
- defrost():解冻被冻结的类,使其可以被修改
- isPrimitive():是否为Java原始类型:boolean, byte, char, short, int, long, float, double, or void.
- getName():获取CtClass代表的类的全限定类名
- setName():修改类名,必须以全限定类名的形式传递参数
- isInterface()&isAnnotation()&isEnum():CtClass代表的类对象是否为接口、注解、枚举类型
- getField()&getMethod()&getConstructor():获取类的字段、方法、构造函数
- addField()&addMethod()&addConstructor():向类中添加字段、方法、构造函数
- removeField()&removeMethod()&removeConstructor():从类中移除字段、方法、构造函数
- insturment():向类中注册CodeConverter或者ExprEditor,其中CodeConverter用于代码的转换,ExprEditor用于编辑方法体中满足条件的代码块,instrument(ExprEditor)是CtMethod和CtConstructor调用的方法
- toClass()&toBytecoded()&writeFile():将CtClass对象转换为Class对象、bytecode字节码、写入.class文件中,调用这些方法后,CtClass对象将会被冻结,无法进行修改
- detach():从ClassPool中移除CtClass对象,释放内存
对于CodeConverter和ExprEditor来说,可以通过如下例子加深理解:
/*
CodeConverter为方法体的简单转换器
此类的实例指定如何修改表示方法体的字节码
*/
ClassPool cp = ClassPool.getDefault();
CtClass point = cp.get("Point");
CtClass singleton = cp.get("Singleton");
CtClass client = cp.get("Client");
CodeConverter conv = new CodeConverter();
conv.replaceNew(point, singleton, "makePoint");
client.instrument(conv);
上述代码的作用为:将Client类的所有方法中出现的"new Point()“替换为"Singleton.makePoint()”
/*
ExprEditor为方法体转换器
可以定义该类的子类自定义修改方法体,如果调用CtMethod.instrument方法,会从头到尾扫描方法体
当发现表达式,例如方法调用、new Object()等被发现,可以调用ExprEdit.edit()方法修改这些表达式
这些修改会作用到原始方法体
*/
public class Func
public static String getName()
String action = "开始";
int length = action.length();
System.out.println(length);
return "张三!!!";
public static void updateMethod() throws Exception
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("Func");
CtMethod getName = ctClass.getDeclaredMethod("getName", new CtClass[]);
getName.instrument(
new ExprEditor()
@Override
public void edit(MethodCall m) throws CannotCompileException
System.out.println(m.getClassName());
if (m.getClassName().equals("java.lang.String")
&& m.getMethodName().equals("length"))
m.replace("$_ = 100;");
);
Class<?> aClass = ctClass.toClass();
Method method = aClass.getMethod("getName", new Class[]);
Object invoke = method.invoke(aClass, null);
System.out.println(invoke);
public static void main(String[] args)
// 该方法会输出100,因为这里int length = action.length()被改为了int length = 100
updateMethod();
通过CtClass对象,我们可以根据实际业务需求对真实类对象进行修改
有关更多CtClass对象的用法,我们可以在实际的业务需求中进行探索
2.3 CtField
CtField代表类的字段,我们既可以通过CtClass获取类中已经存在的字段,也可以新建一个CtField对象并添加到CtClass中
CtField的常用方法如下:
- make(String src, CtClass declaring):通过源码的形式构造CtField对象,例如src可以为:“public String name;”,不要忘记";",否则会编译失败,declaring代表我们要添加字段的类
- getName&setName:获取字段名&修改字段名
- getModifiers&setModifiers:获取访问修饰符&设置访问修饰符,其中,设置访问修饰符可以通过Modifier中提供的常量字段设置,并且可以组合设置,例如要设置public static,可以使用如下设置:Modifier.PUBLIC | Modifier.STATIC
- getAnnotation&getAnnotations:获取字段上的注解
- getSignature:返回代表字段类型的字符串,如果是String类型的话,返回结果为:Ljava/lang/String;
- getGenericSignature&setGenericSignature:获取和设置字段的泛型类型
- getConstantValue:只能获取Java基本类型的包装类型对应的常量值,如果是其他类型的常量的话则会返回null
我们在向CtClass类中添加新字段的时候,可以添加字段的初始值,在调用CtClass.addField方法时,向其中传入CtField.Initializer参数即可设置字段对应的初始值,例如:
ctClass.addField(field, CtField.Initializer.constant("张三"));
此外,如果我们不使用CtField.make()方法创造CtField对象的话,我们直接调用CtField的构造函数即可,然后调用setModifiers设置字段的访问修饰符即可:
CtField username = new CtField(pool.get("java.lang.String"), "username", ctClass);
username.setModifiers(Modifier.PRIVATE);
ctClass.addField(username, CtField.Initializer.constant("ZhangSan"));
2.4 CtConstructor
CtConstructor代表类的构造函数,也可能代表类的初始化函数;可以通过isClassInitializer()方法确定是否为类初始化函数。
我们可以通过CtClass对象获取其对应的CtConstructor,也可以直接新建一个CtConstructor并添加到CtClass对象中;新建CtConsturctor有两种方式:
// 方法一,通过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);
CtConstructor常用方法如下:
- getLongName()&getName():获取带有参数的名称(全限定类名和构造函数参数类型),如:javassist.CtConstructor(CtClass[],CtClass);getName只获取构造函数名也就是类名
- isConstructor()&isClassInitializer():是否为构造函数&是否为类的初始化函数
- callSuper():是否调用父类构造函数
- setBody():设置构造函数源码
- insertBeforeBody():在构造函数开头插入代码
- setModifiers():设置访问权限
通过上述方法,我们可以自定义构造函数并添加到CtClass对象中去,也可以访问现有构造函数的相关信息
CtNewConstructor这个工具类也非常有用,我们通常使用它来创建CtConstructor对象,其常用方法如下:
- make():构造CtNewConsturctor对象
- copy():利用原有的CtConstructor对象复制一个新的CtConstructor对象
- defaultConstructor():创建默认构造函数
- skeleton:创建一个带有参数的构造函数,但是该函数体只调用super()方法,其余代码需要自己编写插入
通过上述代码我们很容易自定义构造函数
2.5 CtMethod
CtMethod代表Java类中的方法,我们通常写的Java类是通过各种各样的方法来实现想要的功能的,所以CtMethod至关重要
CtMethod的创建方法和CtConstructor的创建方法类似,也可以通过源码形式直接创建;或者先定义函数名称和参数类型,然后再设置函数体
CtMethod的一些常用方法如下:
- make():通过源码的形式创建CtMethod对象
- getName()&setName():获取函数名,设置函数名(可以用于修改函数名称)
- setBody():设置函数体
- setWrappedBody():通过提供的CtMethod对象的函数体来设置当前CtMethod对象的函数体
- setModifiers():设置函数的访问修饰符
- insertBefore()&insertAfter()&insertAt():在函数最开始|结束位置|任意位置(需要知道代码行号)插入代码
- addCatch()&addLocalVariable():添加try catch语句 | 添加局部变量
- instrument():根据提供的CodeConverter|ExprEditor对函数体中符合条件的代码进行编辑,详情可以参考2.2节
传递给方法 insertBefore() ,insertAfter() ,addCatch() 和 insertAt() 的 String 对象是由Javassist 的编译器编译的。 由于编译器支持语言扩展,以 $ 开头的几个标识符有特殊的含义:
符号 | 含义 |
---|---|
$0,$1,$2,… | $0代表this,$1,$2,…代表方法的第1个参数、第2个参数… |
$args | 方法参数数组,$args的类型是Object[] |
$$ | 代表全部方法参数,例如m($$)=m($1,$2,…) |
$cflow | cflow变量,此只读变量返回特定方法的递归调用的深度 |
$r | 函数返回结果类型,用于强制类型转换 |
$w | 包装类型,用于强制类型转换,例如:Integer i = ($w)5; |
$_ | 函数返回结果 |
$sig | Class类型的数组,代表形参的类型 |
$type | 代表函数返回结果的Class类型 |
$class | 代表当前编辑的类的Class类型 |
$proceed | 代表源码中调用的方法名、构造函数名… |
2.5.1 $cflow
上述标识符中,对于 c f l o w 这 个 标 识 符 , 我 们 初 看 可 能 会 云 里 雾 里 , cflow这个标识符,我们初看可能会云里雾里, cflow这个标识符,我们初看可能会云里雾里,cflow表示控制流,此只读变量返回特定方法的递归调用的深度。
假设下面所示的方法由CtMethod对象cm表示:
int fact(int n)
if (n <= 1)
return n;
else
return n * fact(n - 1);
要想使用cflow,首先需要声明使用cflow监视方法fact()的调用:
CtMethod cm = ...;
cm.useCflow("fact");
useCflow()的参数是$cflow变量的标识符,任何有效的Java名称都可以用作标识符。
c f l o w ( f a c t ) 表 示 由 c m 指 定 的 方 法 的 递 归 调 用 深 度 。 cflow(fact)表示由cm指定的方法的递归调用深度。 cflow(fact)表示由cm指定的方法的递归调用深度。cflow(fact)的值在方法第一次调用时为0,而当方法在方法中递归调用时为1.
$cflow的值是当前线程的最顶层堆栈下与cm相关联的堆栈帧数。cflow也可以不在cm方法中访问。
2.5.2 addCatch
如何在方法中插入try catch语句呢,通过下面这个例子就能轻松解决:
CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.io.IOException");
// 在插入的源代码中,异常用$e表示
m.addCatch("System.out.println($e); throw $e;", etype);
上述代码转换成的java代码如下(请注意,插入的代码片段必须以 throw 或 return 语句结束):
try
// the original method body
catch (java.io.IOException e)
System.out.println(e);
throw e;
2.5.3 相互递归的方法
有时候我们可能需要向代码中插入相互递归的方法,Javassist也帮我们想到了对应的解决方法
在Javassist中不能有这样的方法:如果它调用另一个方法,而另一个方法没有被添加到一个类(Javassist可以编译一个以递归方式调用的方法)。如果要向类添加相互递归的方法,需要使用下面的技巧。
假设我们想要将方法m()和n()添加到由ctClass表示的类中:
CtClass ctClass = ...;
CtMethod m = CtNewMethod.make("public abstract int m(int i);", ctClass);
CtMethod n = CtNewMethod.make("public abstract int n(int i);", ctClass);
ctClass.addMethod(m);
ctClass.addMethod(n);
m.setBody(" return ($1 <= 0) ? 1 : (n($1 - 1) * $1); ");
n.setBody(" return m($1); ");
cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);
此时,我们必须先创建两个抽象的方法,并将它们添加到类中,然后设置它们的方法体,最后将类改为非抽象类
3. other
如果事先知道要修改哪些类,那么最简单的方法如下:
- 通过ClassPool.get()获取CtClass对象
- 然后对CtClass对象进行修改
- 调用CtClass对象的writeFile()或者toBytecode()获得修改过的类文件
如果在加载时,可以确定是否要修改某个类,就必须使Javassist与类加载器协作,以便在加载时修改字节码;同时也可以自定义类加载器,或者使用Javassist提供的类加载器
3.1 CtClass.toClass()
CtClass 的 toClass() 方法请求当前线程的上下文类加载器,加载 CtClass 对象所表示的类。要调用此方法,调用者必须具有相关的权限; 否则,可能会抛出 SecurityException。例如:
public class Hello
public void say()
System.out.println("Hello");
public class Test
public static void main(String[] args) throws Exception
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("Hello");
CtMethod m = cc.getDeclaredMethod("say");
m.insertBefore(" System.out.println(\\"Hello.say():\\"); ");
Class c = cc.toClass();
Hello h = (Hello)c.newInstance();
h.say();
Test.main()向Hello的say()方法中插入一个println(),然后构造一个修改过的Hello类的实例,并在该实例上调用say()方法
上面的程序要正常运行,Hello类在调用toClass()之前就不能被加载;如果JVM在toClass()调用之前加载了原始的Hello类,后续加载修改的Hello类将会失败(抛出LinkageError)
例如,如果Test.main()方法如下:
public static 字节码增强之Javassist