> 文档中心 > Android编译时技术(一)Javassist 使用

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.运行代码

效果如下:

可根据自己想要测试的方法自行修改