Skywalking分布式追踪与监控
1.1、字节码简述
1.1、字节码简述
Java和C语言很大的不同是C语言不是跨平台的,C语言编译后就是对应CPU的汇编指令,不同操作系统的汇编指令有差异,所以无法跨平台。而Java语言编译之后是字节码,字节码需要通过Java虚拟机来运行,而不同操作系统的Java虚拟机是定制的,所以针对不同的操作系统,JVM会将相同格式的字节码翻译成对应操作系统的汇编指令运行,所以JVM不是跨平台的,但是Java代码是可以跨平台运行的。所以Java跨平台最大原因就是因为编译成了字节码,从而实现“一次编译,到处运行”的特点。
字节码文件需要通过javac命令来将.java文件编译成.class文件,.class文件中存储的就是字节码
字节码的格式是十六进制数字组成,JVM每次读取一个字节也就是两个十六进制数字来加载字节码,最终将字节码在转换成对应操作系统的命令去执行。
Java代码到最终运行的完整流程如下图示:
完成步骤如下:
1、java代码通过Javac命令由java编译器编译成字节码.class文件
2、JVM的类加载器加载.class文件并解析
3、字节码完整性和合法性校验
4、字节码文件运行,实际是转换成操作系统的指令
5、调用对应的操作系统的指令实现java代码的运行
2、字节码文件格式
字节码格式固定,通常包含十个步骤,从前往后依次如下图示:
魔数(magic):魔数是.class文件的开头,占据4个字节固定值不变,值为0xCAFEBABE,由于Java之父James Gosling定的,为Cafe Babe(咖啡宝贝),而对应了Java的标志是一杯咖啡
版本号(version): 魔数后面4个字节是Java版本号,前两个字节是次版本号;后两个字节是主版本号
常量池(constant_pool): 常量池用于存储类中的常量,前两个字节存储计数器,表示常量的个数;后面的字节依次存储常量的具体数据
访问标志(access_flag): 用于标记当前类是否被public、abstract、final、static等关键字修饰,采用位运算来存储,每一位代表一种标志
类索引(this_class):类的全名存储在常量池中,类索引通过索引指向常量池中的地址
父类索引(super_class): 父类全名存储在常量池中,父类索引指向常量池中的地址
接口索引(interfaces): 接口索引前两个字节存储接口数量,后面依次存储各个接口对应的常量池中的地址
字段表(fields): 存储类或接口定义的静态和非静态变量,前两个字节存储字段个数,后面依次存储各个字段的详细信息
方法表(methods): 存储类或接口定义的方法,前两个字段存储方法个数,后面依次存储各个方法的详细信息
附件属性(attributes): 存放在该文件中类或接口所定义属性的基本信息
1.2、字节码增强
字节码增强技术就是对现有的字节码文件进行功能增强,一般方式有两种,一种是直接修改字节码文件,一种是动态创建一个新的字节码文件。
目前使用到了字节码增强技术有ASM、Javassist、AspectJ和Java Proxy, ByteBuddy等
2.1、ASM
ASM可以直接生成字节码文件,也可以动态修改字节码文件,Spring的AOP的cglib就是基于ASM实现的。
由于字节码的格式是固定的,所以ASM可以根据字节码数据结构读取对应位置的数据,然后再修改对应位置的数据即可。
ASM框架涉及的方法都和字节码指令级相关,因此对于指令不熟悉的话很难适应ASM框架。所以最好是有直接基于Java编码的方式对字节码文件进行修改,对于字节码文件的内容进行抽象,javasisst框架就是很好的选择。
2.2、javasisst
javasisst进行字节码增强时,使用者无需关心字节码的结构和指令语法,分别采用ctClass、ctMethod、ctField对类、方法和属性进行抽象,分别调用对应的方法即可完成字节码增强逻辑。
如针对方法进行增强,那么简单增强逻辑代码如下案例:
CtClass ctClass = ClassPool.getDefault().getCtClass("com.lucky.test.User");
CtMethod method = ctClass.getDeclaredMethod("getUserName");
method.insertBefore("System.out.println(\"start\")");
method.insertBefore("System.out.println(\"end\")");
Class clazz = ctClass.toClass();
ctClass.writeFile("/xxx/xxx/xxxx");
逻辑比较简单先根据全路径类名获取CtClass对象,然后根据方法名获取CtMethod对象,在调用对应的插入逻辑方法分别在对应方法前后插入增强逻辑,最好在调用CtClass的写入文件方法将当前类逻辑写入文件生成字节码文件。
1.3、动态字节码增强
ASM和Javasisst都可以创建字节码文件和修改字节码文件,对应的目标都是字节码文件。也就是说在JVM加载字节码文件之前就需要对字节码文件进行修改,而一旦JVM类加载器将目标类加载了,然后此时就无法通过修改字节码文件的方式来实现动态的字节码增强了。
所以如果想要在JVM加载字节码文件之后还进行字节码增强,就需要适应到JDK提供的Instrument。Instrument通常可以配合Javaagent一块使用。
由于在上述提到的各种技术都难以很好的支持在对原有项目无任何修改下完成MOCK功能的需求,在查阅资料后,将目光放至了java agent技术。
java agent本质上可以理解为一个插件,该插件就是一个精心提供的jar包,这个jar包通过JVMTI(JVM Tool Interface)完成加载,最终借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改。
java agent技术的主要功能如下:
通过java agent技术进行类的字节码修改最主要使用的就是Java Instrumentation API。下面将介绍如何使用Java Instrumentation API进行字节码修改。
2.2.1 实现agent启动方法
Java Agent支持目标JVM启动时加载,也支持在目标JVM运行时加载,这两种不同的加载模式会使用不同的入口函数,如果需要在目标JVM启动的同时加载Agent,那么可以选择实现下面的方法:
[
1]
public static void premain(String agentArgs, Instrumentation inst);
[
2]
public static void premain(String agentArgs);
JVM将首先寻找[1],如果没有发现[1],再寻找[2]。如果希望在目标JVM运行时加载Agent,则需要实现下面的方法:
[
1]
public static void agentmain(String agentArgs, Instrumentation inst);
[
2]
public static void agentmain(String agentArgs);
这两组方法的第一个参数AgentArgs是随同 “–javaagent”一起传入的程序参数,如果这个字符串代表了多个参数,就需要自己解析这些参数。inst是Instrumentation类型的对象,是JVM自动传入的,我们可以拿这个参数进行类增强等操作。
2.2.2 指定Main-Class
Agent需要打包成一个jar包,在ManiFest属性中指定“Premain-Class”或者“Agent-Class”,且需根据需求定义Can-Redefine-Classes和Can-Retransform-Classes:
Manifest-Version:
1.0
preMain-Class:
com.test.AgentClass
Archiver-Version:
Plexus
Archiver
Agent-Class:
com.test.AgentClass
Can-Redefine-Classes:
true
Can-Retransform-Classes:
true
Created-By:
Apache
Maven
3.3.9
Build-Jdk:
1.8.0_112
2.2.3 agent加载
try {
String jvmPid = 目标进行的pid;
logger.info("Attaching to target JVM with PID: " + jvmPid);
VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentFilePath);//agentFilePath为agent的路径
jvm.detach();
logger.info("Attached to target JVM and loaded Java agent successfully");
} catch (Exception e) {
throw new RuntimeException(e);
}
2.2.4 Instrument
instrument是JVM提供的一个可以修改已加载类的类库,专门为Java语言编写的插桩服务提供支持。它需要依赖JVMTI的Attach API机制实现。在JDK 1.6以前,instrument只能在JVM刚启动开始加载类时生效,而在JDK 1.6之后,instrument支持了在运行时对类定义的修改。要使用instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。接口中的transform()方法会在类文件被加载时调用,而在transform方法里,我们可以利用上文中的ASM或Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回。
首先可以定义如下的类转换器:
public class TestTransformer implements ClassFileTransformer {
//目标类名称, .分隔
private String targetClassName;
//目标类名称, /分隔
private String targetVMClassName;
private String targetMethodName;
public TestTransformer(String className,String methodName){
this.targetVMClassName = new String(className).replaceAll("\\.","\\/");
this.targetMethodName = methodName;
this.targetClassName=className;
}
//类加载时会执行该函数,其中参数 classfileBuffer为类原始字节码,返回值为目标字节码,className为/分隔
public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//判断类名是否为目标类名
if(!className.equals(targetVMClassName)){
return classfileBuffer;
}
try {
ClassPool classPool = ClassPool.getDefault();
CtClass cls = classPool.get(this.targetClassName);
CtMethod ctMethod = cls.getDeclaredMethod(this.targetMethodName);
ctMethod.insertBefore("{ System.out.println(\"start\"); }");
ctMethod.insertAfter("{ System.out.println(\"end\"); }");
return cls.toBytecode();
} catch (Exception e) {
}
return classfileBuffer;
}
}
类转换器定义完毕后,需要将定义好的类转换器添加到对应的instrmentation中,对于已经加载过的类使用retransformClasses对类进行重新加载:
public class AgentDemo {
private static String className = "hello.GreetingController";
private static String methodName = "getDomain";
public static void agentmain(String args, Instrumentation instrumentation) {
try {
List needRetransFormClasses = new LinkedList<>();
Class[] loadedClass = instrumentation.getAllLoadedClasses();
for (int i = 0; i < loadedClass.length; i++) {
if (loadedClass[i].getName().equals(className)) {
needRetransFormClasses.add(loadedClass[i]);
}
}
instrumentation.addTransformer(new TestTransformer(className, methodName));
instrumentation.retransformClasses(needRetransFormClasses.toArray(new Class[0]));
} catch (Exception e) {
}
}
public static void premain(String args, Instrumentation instrumentation) {
instrumentation.addTransformer(new TestTransformer(className, methodName));
}
}
从上图的代码可以看出,主方法实现了两个,分别为agentmain和premain,其中