什么是插桩
最简单的理解就是通过某种策略,在源代码的基础上替换或者插入另外一些代码。
应用场景
方法监控耗时、性能分析、获取运行时信息、动态调试、热修复。
常用框架
- javaassist
特点:简单,性能比asm低
是一个开源的分析、编辑和创建java字节码的类库。性能消耗较⼤大,使⽤用容易。
- asm
特点:复杂,性能高,一般更为常用
是一个轻量及java字节码操作框架,直接涉及到JVM底层的操作和指令,性能高,功能丰富
- BCEL
这是Apache Software Fundation的jakarta项目的一部分。BCEL它可以让你深入JVM汇编语言进行类的操作的细节。
插桩原理
java 5之后新加了一个包java.lang.instrument
,基本上大多数的动态修改类都是在此基础上实现的
(基于jvmti,小部分可能直接使用jvmti实现)
它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。
Instrumentation 的基本功能
java.lang.instrument
包的具体实现,依赖于 JVMTI
入口类是 java.lang.instrument
的 Instrumentation
JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。
Instrumentation 的最大作用,就是类定义动态改变和操作。
Instrumentation 用法
使用Instrumentation 代理,主要分为以下几个步骤
编写 premain 函数
编写一个 Java 类,包含如下两个方法当中的任何一个
public static void premain(String agentArgs, Instrumentation inst); [1]
public static void premain(String agentArgs); [2]
其中[1]执行的优先级是高于[2]的。
在这个permain函数中,开发者是可以对加载的类进行各种操作。
agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。与 main 函数不同的是,这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。
Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。
java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分
集中了其中几乎所有的功能方法,例如类定义的转换和操作等等
jar 文件打包
将这个 Java 类打包成一个 jar 文件
并在其中的 manifest 属性当中加入" Premain-Class"来指定步骤 1 当中编写的那个带有 premain 的 Java 类。
运行
用如下方式运行带有 Instrumentation 的 Java 程序:
java -javaagent:jar 文件的位置 [= 传入 premain 的参数 ]
实际例子(打印以下加载的类)
- 首先定义 MANIFEST.MF 文件
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: qunar.tc.Main
- 创建一个Premain-Class 指定的类,类中包含 premain 方法:
public class Main {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("agentArgs : " + agentArgs);
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
System.out.println("premain load Class :" + className);
return classfileBuffer;
}
}, true);
}
}
3 使用maven或者gradle打包成jar,包含刚刚定义的MANIFEST.MF 文件和包含premain函数的类。
4 使用参数 -javaagent:/jar包路径=[agentArgs 参数] 启动目标程序。
运行之后可以看到打印了许多加载的类。
和插桩有什么联系
我们可以看到获取了一个Instrumentation实例,这个是最重要的一个类。
常用的api
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。void redefineClasses(ClassDefinition... definitions) hrows ClassNotFoundException, UnmodifiableClassException;
在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。boolean removeTransformer(ClassFileTransformer transformer)
删除一个类转换器void retransformClasses(Class<?>... classes) throws UnmodifiableClassException
在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。void appendToBootstrapClassLoaderSearch(JarFile jarfile)
添加jar文件到BootstrapClassLoader中void appendToSystemClassLoaderSearch(JarFile jarfile)
添加jar 文件到 system class loader.Class[] getAllLoadedClasses()
获取加载的所有类数组。
与其结合的类是ClassFileTransformer接口。
这个接口只有一个方法
byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
通过这个接口就可以将原本的类字节码替换,从而实现想要的目标。
attach方法
插桩的jar包已经弄好,接下来就是需要attach目标JVM,让目标JVM加载此jar包,从而实现插桩
主要包括三种方法
- 静态的attach,侵入大,需要目标JVM指定javaagent参数
- 动态attach,侵入小,目标JVM完全无感知
- 直接使用JVMTI接口
指定javaagent参数
启动java程序时,指定agent所在的lib
java -javaagent:agent.jar Main
动态attach
使用VirtualMachine进行attach
public static void main(String[] args) throws Exception
{
VirtualMachine vm = null;
String agentjarpath = "agent.jar"; //agentjar路径
vm = VirtualMachine.attach("1024");//目标JVM的进程ID(PID)
vm.loadAgent(agentjarpath, "This is Args to the Agent.");
vm.detach();
}
直接使用jvmti
前两种方法都是基于JVMTI提供的Java接口,JVMTI是底层实现,所以是可以通过编写c代码实现插桩
怎样修改代码
上面都是讲怎么样实现可以动态加载代码
那么怎样才能动态修改class文件,这时候就需要asm了
因为class文件是及其复杂的,手动修改是基本不能实现的
基本实现
使用asm+ClassFileTransformer类就可以实现修改原class文件。
asm core 包使用
主要是使用asm的core包实现修改类。
ClassVisitor 类
asm使用visiter模式修改类
ClassReader 类
这个类会将 .class 文件读入到 ClassReader 中的字节数组中
它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法
ClassWriter 类
ClassWriter 是一个 ClassVisitor 的子类,是和 ClassReader 对应的类
ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码内容以字节数组的形式输出。
2.4 MethodVisitor & AdviceAdapter
MethodVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Method 时就转入 MethodVisitor 接口处理。
AdviceAdapter 是 MethodVisitor 的子类,使用 AdviceAdapter 可以更方便的修改方法的字节码。
操作流程
需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中
然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写
需要事件过滤器 ClassVisitor。在调用 ClassVisitor 的某些方法时会产生一个新的 XXXVisitor 对象,当我们需要修改对应的内容时只要实现自己的 XXXVisitor 并返回就可以了
总结
主要是使用Instrumentation接口+asm实现了动态修改类,从而实现插桩。
实现步骤 :
- 获取Instrumentation对象
两种方法
方法一
启动应用时,指定javaagent参数,将包含premain方法的agent包指定。
局限较大,必须启动应用程序时指定agent包。
方法二
使用VirtualMachine进行attach,优点是对应用无侵入。
- 修改class文件
增加一个ClassFileTransformer类,使用asm修改字节码,实现修改逻辑
使用premain传入的Instrumentation实例的addTransformer方法,添加上一步骤的ClassFileTransformer
调用Instrumentation实例的retransformClasses方法,类重新加载,实现插桩