AOP之Javassist
javassist简介
Javassist作用是在编译器间修改class文件,修改时机是在class文件被转化为dex文件之前去修改。
Javassist提供两个级别的API:源级别和字节码级别,如果用户使用源级别API,他们可以编辑类文件而不需要了解Java字节码的规范。整个API仅使用Java语言的风格进行设计。
您甚至可以以源文本的形式指定插入的字节码; Javassist将即时编译它。
字节码级别API允许用户像其他编辑器一样直接编辑类文件(class file)。
DroidAssist项目
滴滴发布的开源项目 DroidAssist ,提供了一种简单易用、无侵入、配置化、轻量级的 Java 字节码操作方式,只需要在 XML 配置中添加简单的 Java 代码即可实现编译期对 Class 文件的动态修改。
是学习javasist很值得参考的项目,该项目主要实现的功能
pic1.png
主要类
- CtClass类是Java字节码文件的抽象表示
- ClassPool对象是表示类文件的CtClass对象的容器,它根据需要读取类文件以构造CtClass对象
- CtMethod 方法
- CtConstructor
- CtField 域成员
ClassPool
ClassPool classPool= ClassPool.getDefault();
CtClass ctClass = classPool.get("com.old.class");
ClassPool是CtClass对象的哈希表,它使用类名作为键。 ClassPool中的get()搜索此哈希表以查找与指定键关联的CtClass对象。如果没有找到,get()将读取类文件来构造一个新的Ctclass对象并存储在在哈希表中然后返回该对象。
定义一个新类
要从头开始定义新类,必须在ClassPool上调用makeClass()
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("newClass");
添加新方法或域
newClass成员方法可以使用在CtNewMethod中声明的工厂方法创建,并使用CtClass中的addMethod()添加到newClass类。
冻结类
如果通过writeFile(),toClass()或toBytecode()将CtClass对象转换为类文件,Javassist将冻结该CtClass对象。不允许对该CtClass对象进行进一步修改。这是为了在开发人员尝试修改已加载的类文件时警告开发人员,已经被JVM载入的类不允许被重新载入。
ctClass.defrost();
当调用defrost()之后,CtClass对象将再次可以被修改。
但是一旦CtClass对象被writeFile()或toBytecode()转换为类文件,Javassist就会拒绝对该CtClass对象的进一步修改
在运行时重新加载类
Javassist提供了一个方便的类,用于在运行时重新加载类。有关更多信息,请参阅javassist.tools.HotSwapper的API文档。
CtMethod
CtMethod和CtConstructor提供了insertBefore(),insertAfter() 和 addCatch() 方法将代码片段插入到已经存在的方法的方法体中,
addCatch() 指的是在方法中加入try catch块
因为Javassist包含一个用于处理源文本的简单Java编译器。它接收用Java编写的源文本并将其编译为Java字节码,然后将该字节码将内联到方法体中,换而言之像ASM那样插入字节码的这一步javassist替我们做了。也正以为如此我们可以使用javassist的语言扩展功能,在插入的源文本中以$开头的多个标识符具有特殊含义
- $0, $1, $2, …:代表this(非静态方法)和函数的参数
- $args:代表方法的参数数组,类型是Object[]
- $cflow:表示递归调用的深度
- $r:方法返回结果类型,用来做强制类型转换,比如:Object -result = … ; $_ = ($r)result;
- $_:代表该方法的结果的值,类型是方法的返回类型,如果返回类型为void,则$ _的类型为Object,$ _的值为null。注意该变量只能在CtMethod 的insertAfter() 和 CtConstructor中插入代码时使用,不能在CtMethod 的insertBefore()中使用
- $sig:表示方法声明的形参类型的数组
- $class:表示当前正在修改的方法所在的类
修改方法体
CtMethod和CtConstructor提供setBody()来替换整个方法体,用来将给定的源文本代码编译为java字节码然后替换整个方法体,因此该API也是通过java源文本的形式插入代码,如果给定的源文本为空,则替换后的主体只包含一个返回语句,该语句返回零或空值,除非结果类型为void。setBody()同样可以使用_不可用外,其余都可使用。
减少内存溢出
ClassPool是一个CtClass objects的装载容器。当加载了CtClass object后,是不会被ClassPool释放的(默认情况下)。这个是因为CtClass object 有可能在下个阶段会被用到。
当加载过多的CtClass object的时候,会造成OutOfMemory的异常。为了避免这个异常,javassist提供几种方法,一种是在上面提到 的 ClassPool.doPruning这个参数,还有一种方法是调用 CtClass.detach()方法,可以把CtClass object 从ClassPool中移除。
CtClass cc = ... ;
cc.writeFile();
cc.detach();
限制
- 不支持java5.0的新增语法。不支持注解修改,但可以通过底层的javassist类来解决,具体参考:javassist.bytecode.annotation
- 不支持数组的初始化,如String[]{"1","2"},除非只有数组的容量为1
- 不支持内部类和匿名类
- 不支持continue和btreak 表达式
总结
ASM和javassist各具优点,javassist包含一个用于处理源文本的简单Java编译器,为我们提供了语言扩展功能,在插入的源文件文件中以$开头的多个标识符具有特殊含义,这些语言扩展特性在某些业务场景下大大简化了我们插入指令的逻辑。
在不同场景下使用合适的框架能够大大减少我们的代码开发量和排错成本。一般说来如果要插入代码/指令的场景具备以下2个条件时建议使用javassist。
- 要插入的字节码指令不是固定的,会随着函数形参个数,类型,顺序,函数返回值类型等动态改变的场景下
- 直接替换原函数的整个方法体实现,而不是在原实现前面或者后面插入一段新的代码片段