前言
在上篇文章通过实战走近Java Agent探针技术中,在进行热替换的时候,我们使用了javasist对目标类的字节码进行了增强,所谓字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。字节码增强技术的实现有很多,比如:ASM、cglib、javasist等、上篇中用到了javasist去修改我们目标类的行为,但是并没有着重介绍javasist的具体使用,本篇文章将带你走近字节码增强艺术:javasist
走近Javasist
官网地址:http://www.javassist.org/
GitHub:https://github.com/jboss-javassist/javassist
Javasist(Java Programming Assistant)是我们操作字节码更加简单。所谓Javasist其实就是一个类库,它提供了允许开发者在运行时去定义一个新class或者修改一个class文件。那么javasist提供了两种级别的API:源码级别和字节码级别。所谓源码级别,说白点就是更加方便我们傻瓜式去调用,我们不用去关注字节码实现规范细节,这也是Javasist的一大优势,编程简单,提升效率,谁不爱呢;而相应的字节码级别对我们的要求就会高一点,它允许我们像在编辑器中写代码一样去编写字节码文件。
写此文中,特意去看了一下Javasist社区 https://github.com/jboss-javassist/javassist/releases ,基本一年两三个版本,所以还是蛮活跃的,暂时不必担心开源版本无人维护而难以选择。那么废话到这,接下来就进入实战环节了!
Javasist实战
首先引入pom依赖:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
整个Javasist操作字节码有四个类是最为关键和核心的,其实如果你看一个类的组成(方法+属性),大概也就知道都有哪些东西:
类名 | 描述 |
---|---|
Javassist.CtClass | Javassist.CtClass就代表着一个class文件的抽象,一个CtClass对象就是操作一个class文件的句柄 |
ClassPool | ClassPool可以看作是保存着CtClass对象的一个容器,当我们需要获取指定类对应的CtClass时,就可以通过指定类的全限定名去获取。要对字节码进行操作,第一步就是要从ClassPool中获取对应的CtClass。从开发视角看ClassPool是一个CtClass对象的哈希表,类名作为键,CtClass作为value |
CtMethod | 一个实例代表一个方法 |
CtField | 一个实例代表一个属性 |
有了关键类之后,再来看下这些关键类(主要就看下ClassPool和CtClass)哪些关键API吧:
CtClass
标题 | |
---|---|
writeFile() | translates the CtClass object into a class file and writes it on a local disk. |
toBytecode() | 获取字节码 |
toClass() | 获取当前执行线程的contextClassLoader对该字节码文件进行加载 |
setName() | 从名字上看是修改类名,但是这里其实是基于当前CtClass复制一个出来,所以这里set的是新类的名字 |
defrost() | 如果一个CtClass对象被writeFile()、toClass()或toBytecode()转换成一个类文件,Javassist将冻结该CtClass对象。不允许进一步修改该CtClass对象。这是为了在开发人员试图修改已经加载的类文件时发出警告,因为JVM不允许重新加载类。那么我们可以通过此方法对该CtClass进行解冻,之后就可以修改了 |
detach() | 需要注意的是ClassPool在运行期间,所有创建的CtClass都会永远保存。如果CtClass对象的数量变得惊人地大,ClassPool的这种规范可能会导致巨大的内存消耗(这种情况很少发生,因为Javassist试图以各种方式减少内存消耗)。为了避免这个问题,您可以显式地从ClassPool中删除一个不必要的CtClass对象。如果在一个CtClass对象上调用detach(),那么该CtClass对象将从ClassPool中删除。当然,除了这种方式去避免这种问题,还有另外一种方式:当前ClassPool直接不要了,当垃圾回收时,那么所有相关的CtClass也都没了 |
ClassPool
标题 | |
---|---|
getDefault() | 通过getDefault()获取的ClassPool,其默认搜索路径是System Path |
insertClassPath | 假如我们的javasist运行在诸如Jboss,Tomcat这种自定义了自己加载器的实现,我们如果通过getDefault获取时,是无法获取到一些路径下的类的,此时我们可以通过该方法去指定一些搜索路径 |
从上面insertClassPath可以知道,是不是感觉和类加载有点像呢,说白了就是找class文件嘛,那怎么找?类加载机制提供了双亲委派,然而javasist也提供了类似的机制,我们可以给ClassPool指定父ClassPool,这样,每当我们调用get()时,都会优先从父ClassPool中去寻找,就像这样:
ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.insertClassPath("./classes");
然后javasist除了这,还支持从儿子往爷爷头上找,就是自己先找,自己找不到,再问自己爹找,这个可以通过属性childFirstLookup去控制。
代码实战
说了这么多,接下来直接上代码
通过Javasist修改已有类的行为
这里先定义一个我们目标要进行修改的类:
public class TargetObject {
public void sayHello(){
System.out.println("hello");
}
}
接下来我将通过javasist改变sayHello的行为,就像是我们平常使用的AOP一样:
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get("com.cf.study.DailyStudy.javasisttest.TargetObject");
CtMethod method = ctClass.getDeclaredMethod("sayHello");
String beforeExecute = "System.out.println("before...");";
String afterExecute = "System.out.println("after...");";
method.insertBefore(beforeExecute);
method.insertAfter(afterExecute);
TargetObject targetObjectAgent = (TargetObject)ctClass.toClass().getConstructor().newInstance();
targetObjectAgent.sayHello();
输出:
before...
hello
after...
参考
https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
http://www.javassist.org/tutorial/tutorial.html
网友评论