Android编译时技术(一)Javassist 使用
一、前言
所谓的Javassist,其实就是如何生成一个Class文件或者修改一个Class文件的工具,包括对Class里的成员变量或者方法进行增加或修改。相比于ASM,Javassist最大的好处就是方便,简单,不用去关心字节码操作。
二、用Javassist生成文件
首先,先简单生成一个Class文件。运行以下代码,就可以直接生成一个Class文件。
public class GenClass { @Test public static void main(String[] args) throws Exception { //创建一个字节码池,用来存放生成的Class ClassPool pool = ClassPool.getDefault(); // 创建一个User类 CtClass clz = pool.makeClass("cn.example.User"); //写入本地 clz.writeFile(); }}
可以看到,在根目录下直接生成了一个User.Class文件。如下:
先解释一下上面代码的作用:
- 首先,ClassPool.getDefault(),用来存放我们生成的Class文件,把他加载进内存。
- 其次,pool.makeClass(),创建一个Class文件,它还有pool.get(),获取一个Class文件。
- 最后,clz.writeFile(),把Class文件输出出来。可传path,不传就默认输出到根目录。
很明显,如果我们想对Class进行自定义添加操作,那肯定是对clz对象进行操作,可以看看他有哪些方法,如下:
有了方法,那接下来我们看看如何给Class添加构造方法、成员变量、方法和接口。
2.1 添加成员变量
// 创建一个String变量name CtField nameField = new CtField(pool.get("java.lang.String"), "name", clz); // 设置name成员变量为私有属性,不设默认Public nameField.setModifiers(Modifier.PRIVATE); // 将name成员变量添加到Person类中 clz.addField(nameField); // 创建一个Integer变量age CtField ageField = new CtField(pool.get("java.lang.Integer"), "age", clz); // 设置name成员变量为私有属性,不设默认Public ageField.setModifiers(Modifier.PRIVATE); // 将name成员变量添加到Person类中 clz.addField(ageField);
CtField方法参数解释:
/** * @param type变量类型(String、Integer、double等,要写全路径) * @param name变量名 * @param declaring 声明这个变量添加到哪个Class上 * */public CtField(CtClass type, String name, CtClass declaring)
效果如下:
2.2 添加构造方法
// 添加一个无参数构造方法 CtConstructor defaultConstructor = new CtConstructor(new CtClass[]{}, clz); // 设置方法体内容 defaultConstructor.setBody("{name = \"\";}"); clz.addConstructor(defaultConstructor); // 添加一个有参数的构造方法 CtConstructor paramsConstructor = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, clz); // 设置方法体内容 $0表示this,$1,$2...表示方法参数 paramsConstructor.setBody("{$0.name = $1;}"); clz.addConstructor(paramsConstructor);
CtConstructor方法参数解释:
/** * @param parameters 构造的参数(String、Integer、double等,要写全路径) * @param declaring 声明这个方法添加到哪个Class上 */public CtConstructor(CtClass[] parameters, CtClass declaring)
效果如下:
2.3 添加方法
// 添加一个sayHello 无参的方法 CtMethod sayHello = new CtMethod(CtClass.voidType, "sayHello", new CtClass[]{}, clz); //设置方法为PRIVATE,默认为PUBLIC sayHello.setModifiers(Modifier.PRIVATE); //设置方法体内容 sayHello.setBody("System.out.println(\"hello, this is \" + $0.name);"); clz.addMethod(sayHello); // 添加一个sayHello 有参的方法 CtMethod sayHelloWithParam = new CtMethod(CtClass.voidType, "sayHello", new CtClass[]{pool.get("java.lang.String")}, clz); //设置方法为PRIVATE,默认为PUBLIC sayHelloWithParam.setModifiers(Modifier.PRIVATE); //设置方法体内容 sayHelloWithParam.setBody("System.out.println(\"hello, this is \" + $1);"); clz.addMethod(sayHelloWithParam);
CtMethod方法参数解释:
/** * @param returnType 返回类型 * @param mname 方法名 * @param parameters 方法参数(String、Integer、double等,要写全路径) * @param declaring 声明这个方法添加到哪个Class上 */public CtMethod(CtClass returnType, String mname, CtClass[] parameters, CtClass declaring)
效果如下:
PS:一个打印的是成员变量name,一个是传进来的变量
三、用Javassist修改Class文件
除了要如何生成一个Class文件外,我们还需要知道如何对一个已有的Class文件进行修改。
3.1 加载Class文件到内存
想要对Class进行操作前,那肯定需要将Class加载进内存,不然操作空气嘛😀。代码如下:
//创建一个字节码池,用来存放加载进来的Class ClassPool pool = ClassPool.getDefault(); //添加Class路径,不能包括包名 pool.appendClassPath("D:\\20210426\\code\\otherCode\\GradlePluginDemo");// pool.insertClassPath("D:\\20210426\\code\\otherCode\\GradlePluginDemo"); // 获取User.Class,不能加.class CtClass clz = pool.get("cn.example.User"); //非常重要的一步,不是自己创建的Class,都需要先调用defrost,才可以进行修改Class。 clz.defrost();
PS:appendClassPath和insertClassPath区别
- 在ClassPool池中有一个搜索列表(链表结构),用来提供给pool.get获取class文件的来源。
- appendClassPath是添加到搜索列表最后。
- insertClassPath是添加到搜索列表最前面,如果先append,在insert,会把之前append的数据放在insert之后。
3.2 删除Class成员变量
删除名字是name的成员变量
for (CtField field : clz.getDeclaredFields()) { System.out.println("field name:"+field.getName()); if (field.getName().equals("name")){ clz.removeField(field); } }
3.3 删除Class构造方法
删除名字是User的构造方法
for (CtConstructor constructor : clz.getDeclaredConstructors()) { System.out.println("constructor name:"+constructor.getName()); if (constructor.getName().equals("User")){ clz.removeConstructor(constructor); } }
3.4 删除Class方法
删除名字是sayHello方法
for (CtMethod method : clz.getDeclaredMethods()) { System.out.println("method name:"+method.getName()); if (method.getName().equals("sayHello")){ clz.removeMethod(method); } }
3.5 写回本地
修改完成别忘记写回本地,释放内存。当然,如果进程都结束了就没必要释放了。
//把修改的内容写入文件 clz.writeFile(fileName) //释放内存 clz.detach()
四、如何测试?
看完了如何生成和修改一个Class文件,那怎么能快速的知道有没有生效呢?(如果你有好的方案欢迎评论)答案如下:
1.在Class类中添加一个main方法,main方法中调用sayHello方法(不需要测试方法的也可以不用调用)。
//添加一个main方法 CtMethod ctMethod = new CtMethod(CtClass.voidType, "main", new CtClass[]{pool.get(String[].class.getName())}, clz); //将main方法声明为public static类型 ctMethod.setModifiers(Modifier.PUBLIC + Modifier.STATIC); //设置方法体 ctMethod.setBody("{" + "sayHello(\"hello, this is \");" + "}"); clz.addMethod(ctMethod);
注意,main方法是static的,那sayHello也要设置成static,代码如下:
sayHelloWithParam.setModifiers(Modifier.PRIVATE+ Modifier.STATIC);
2. 把生成的Class文件实例化出来,genClass也就是上面生成的User.Class文件
public static void main(String[] args) throws Exception { //测试 Class clazz = genClass(); Object obj = clazz.newInstance(); Method mainMethod = clazz.getMethod("main", new Class[]{String[].class}); mainMethod.invoke(obj, new String[1]); }
3.运行代码
效果如下:
可根据自己想要测试的方法自行修改