上章节写到关于 java热部署功能的技术点,简单的阐述了关于类加载的问题,既然了解到了class这个知识点了,那就不能不刨根问底的对class解析一番。看看能不能完成一个类似于ASM、Javassist之类的java字节码操纵功能。
- Class文件是一组以8位字节为基础单位的二进制流,当遇到需要8位字节以上空间的数据项时,则会按照高位在前的方式分隔成若干个8位字节进行存储。各项按照严格顺序连续存放的,它们之间没有任何填充或对齐作为各项间的分隔符号。每个结构体有结构u1 u2 u3 u4规定结构体的长度,分别代表1个字节、2个字节、4个字节、8个字节的无符号数。
结构说明:
符号 | 中文名 | 结构 | 作用 | 规则 |
---|---|---|---|---|
magic | 魔数 | U4 | 所有的由Java编译器编译而成的class文件的前4个字节都是 “0xCAFEBABE” ,JVM用来判断是否是可加载的.class文件 | |
minor_version | 次版本号 | u2 | JVM | |
major_version | 主版本号 | u2 | JVM加载class文件的时候,判断是否可加载,如果JDK.Mj_Version<Class.Mj_Version,则认为加载不了。需要重新编译 | JDK1.0->45;1.7->51 |
constant_pool_count | 常量池中常量数量 | u2 | 记录了constatn_pool中constant_pool_info的数量 | index从1开始;index=0:某些指向常量池的索引值的数据在特定的情况下表达“不引用任何一个常量池项”。 |
constatn_pool | 常量池数据区 | constant_pool_info结构 | 包含Class文件结构及其子结构中引用的所有 字符串常量、类、接口、字段名和其它常量(字面量和符号引用) | tag bytes:第一个字节,用于识别哪种类型的常量。index=constant_pool_count - 1。 |
access_flags | 访问标志 | u2 | 表示某个类或者接口的访问权限及基础属性 | |
this_class | 类索引 | u2 | this_class的值必须是对constant_pool表中项目的一个有效索引值。constant_pool表在这个索引处的项必须为CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类或接口 | |
super_class | 父类索引 | u2 | super_class的值必须是对constant_pool表中项目的一个有效索引值。constant_pool表在这个索引处的项必须为CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的直接父类 | |
interfaces_count | 接口计数器 | u2 | 当前类或接口的直接父类接口数 | |
interfaces | 接口信息数据区(接口表) | u2 | interfaces[]数组中的每个成员的值必须是一个对constant_pool表中项目的一个有效索引值, 它的长度为 interfaces_count | |
fields_count | 字段计数区 | u2 | fields_count的值表示当前Class文件 fields[] 数组的成员个数 | |
fields | 字段信息数据区(字段表) | field_info结构 | fields[]数组中的每个成员都必须是一个fields_info结构 的数据项,用于表示当前类或接口中某个字段的完整描述,但不包括从父类或父接口继承的部分 | |
methods_count | 方法计数器 | u2 | methods_count的值表示当前Class 文件 methods[]数组的成员个数 | |
methods | 方法信息数据区(方法表) | method_info 结构 | methods[] 数组中的每个成员都必须是一个 method_info 结构 的数据项,用于表示当前类或接口中某个方法的完整描述 | |
attributions_count | 属性计数器 | u2 | attributes_count的值表示当前 Class 文件attributes表的成员个数 | |
attributions | 属性信息数据区(属性表) | attribute_info结构 | attributes 表的每个项的值必须是attribute_info结构 | 在Java 7 规范里,Class文件结构中的attributes表的项包括下列定义的属性InnerClasses 、 EnclosingMethod 、 Synthetic 、Signature、SourceFile,SourceDebugExtension 、Deprecated、RuntimeVisibleAnnotations 、RuntimeInvisibleAnnotations以及BootstrapMethods属性。 |
定义一个简单的javaMainHello类
public class MainHello {
private static final String main_type="1";
private String name;
private int age;
public void hello(){
System.out.println("我是新的 MainHello");
}
public String myQQ(){
return "123456";
}
}
以UltraEdit打开calss编译文件,如下。
次版本号是:0000
主版本号是:0034,十进制是52,表示采用的是jdk1.8
- 以class 文件流以下标0开始,从0到8(u4)的位置定义为magic。从8到12(u2)的位置定义为 JDK次版本号。以此类推。只要推动指针位按照class结构体的长度去读取数据,就可以从class流文件中,读取出完整的java 代码信息。
于是我们定义 ClazzAnalysis 类用于解析class字节流
public class ClazzAnalysis {
private int fetchLen = 0; //当前读取结构体的长度
private int start_pointer = 0; //文件指针下标
private String hexString = ""; // 十六进制数据总串
private Class_info info = new Class_info(); // 存放所有数据的 class_info 表
public String fetchClazzTructure(int len) {
fetchLen = len; //当前结构体长度
//从上一个指针位 读取 len 个长度的流数据
String cutStr = hexString.substring(start_pointer, start_pointer + len);
//读取完数据后,指针移到下一个结构指针位
start_pointer = start_pointer + fetchLen;
return cutStr;
}
}
fetchClazzTructure函数为数据读取的代码,代码不多,只要将完整的class文件流按照结构长度,以此去读取就行,难就你难在,这calss的结构真心多,每个结构体中间又引用了其他的结构体,你得把每一个结构体的关系都得理清,不然只要其中一个指针位不对,后面的数据就全不对,笔者捣鼓这结构体代码时,也是弄得想哭啊。
magic、minor_version、major_version读取方式并不复杂。fetchClazzTructure 数据读取完之后,调用DataHandler 接口用于数据的转换和保存。
//魔数
fetchClazzTructure(8, info, new DataHandler() {
@Override
public Object handle(String cutStr, Class_info info) {
info.setMagic(cutStr);
return cutStr;
}
});
//JDK 次版本号
fetchClazzTructure(4, info, new DataHandler() {
@Override
public Object handle(String cutStr, Class_info info) {
String i = Integer.parseInt(cutStr, 16) + "";
info.setMinor_version(i);
return i;
}
});
//JDK 次版本号
fetchClazzTructure(4, info, new DataHandler() {
@Override
public Object handle(String cutStr, Class_info info) {
String i = Integer.parseInt(cutStr, 16) + "";
info.setMajor_version(i);
return i;
}
});
数据往下走,我们就到了calss的常量结构了。
二、class文件常量池
- 所有 符号(变量、方法、类) 都是通过cp_info结构来表示。
- 同一文件的所有符号(变量、方法、类) 的相同值都会指向同一地址
- 对所有的基本类型(int、float、long、double) 都是 字面类型+bytes表示
- 对所有引用类型(String,class),都会用单独的 Constant_utf8_info 构造,然后在通过 Constant_String_info, constant_Class_info 指针只过去
- 每个个类而言,其class文件中至少要有两个CONSTANT_Class_info常量池项,用来表示自己的类信息和其父类(Object)信息
常量池结构
constant_pool_count 定义了此class 常量池的个数,constatn_pool定义常量池数据内容。
读取方式为,从constatn_pool 指针位开始,读取1bit的长度,为tag ,此tag描述了常量的类型。info[]为数据流。在根据tag的类型,再去根据不同类型的结构体去解析info[]流中的内容。
以CONSTANT_Utf8_info为例,constant_pool_count 读取完,指向constatn_pool,读取1bit的长度,tag值为CONSTANT_Utf8_info。指针向前读取2bit数据,意为当前字符串所占用X的长度,于是指针向前读取X个单位,读取的内容就是当前CONSTANT_Utf8_info的常量字符串的值。
读取的方式也不复杂,复杂的地方在于结构体的复杂性,14种常量类型的读取指针位置一个都不能乱,只要其中一个错位了,后续的指针位就完全错位,导致数据读取错误。
于是我们按照常量结构体类型,定义对应的java类型。并定义一个数据读取转换的接口,由每一种数据类型实现onTransform函数,完成不同类型的数据解析。
于是我们定义ConstantFactory 用于根据tag 分发数据类型,完成数据的解析。
public class ConstantFactory {
public Constant_X_info transform(int tag, String tagByte, ClazzAnalysis clazzAnalysis) {
Constant_X_info tagConst = null;
if (tag == 1) {
tagConst = new Constant_Utf8_info();
}else if(tag==3){
tagConst = new Constant_Integer_info();
}else if(tag==4){
tagConst = new Constant_Float_info();
}else if(tag==5){
tagConst = new Constant_Long_info();
}else if(tag==6){
tagConst = new Constant_Double_info();
}else if(tag==7){
tagConst = new Constant_Class_info();
}else if(tag==8){
tagConst = new Constant_String_info();
}else if(tag==9){
tagConst = new Constant_Fieldref_info();
}else if(tag==10){
tagConst = new Constant_Methodref_info();
}else if(tag==11){
tagConst = new Constant_InterfaceMethodref_info();
}else if(tag==12){
tagConst = new Constant_NameAndType_info();
}else if(tag==15){
tagConst = new Constant_MethodHandle_info();
}else if(tag==16){
tagConst = new Constant_MethodType_info();
}else if(tag==18){
tagConst = new Constant_InvokeDynamic_info();
}
if (tagConst != null) tagConst.onTransform(tag, tagByte, clazzAnalysis);
return tagConst;
}
在于是,我们有了一下代码
//常量池数量
fetchClazzTructure(4, info, new DataHandler() {
@Override
public Object handle(String cutStr, Class_info info) {
int cp_count = FileUtil.hex2Integer(cutStr);
info.setCp_count(cp_count);
return cp_count;
}
});
// 5.常量池
Map<Integer, Constant_X_info> constant_pool_Map = new HashMap<Integer, Constant_X_info>();
ConstantFactory constantFactory = new ConstantFactory();
for (int i = 0; i < info.getCp_count() - 1; i++) {
String constTag = fetchClazzTructure(2);
int tag = FileUtil.hex2Integer(constTag);
Constant_X_info constantXInfo = constantFactory.transform(tag, constTag, this);
constant_pool_Map.put(i, constantXInfo);
}
info.setConstan_poolMap(constant_pool_Map);
测试代码如下,为了方便阅读,就只打印了Constant_Utf8_info 其中的内容。通过javac -verbose MainHello.class 命令查看该class的字节码信息。对照检查常量池数据是否吻合。
Microsoft Windows [版本 6.1.7600]
版权所有 (c) 2009 Microsoft Corporation。保留所有权利。
D:\it.work\classLoaderTest>javap -verbose MainHello.class
Classfile /D:/it.work/classLoaderTest/MainHello.class
Last modified 2019-7-24; size 720 bytes
MD5 checksum e3ec731813868c40f58b2dee477ddec4
Compiled from "MainHello.java"
public class cn.app.wuzhi.MainHello
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#27 // java/lang/Object."<init>":()V
#2 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/Print
Stream;
#3 = String #30 // 我是新的 MainHello
#4 = Methodref #31.#32 // java/io/PrintStream.println:(Ljava/
lang/String;)V
#5 = String #33 // 123456
#6 = Class #34 // cn/app/wuzhi/MainHello
#7 = Class #35 // java/lang/Object
#8 = Utf8 main_type
#9 = Utf8 Ljava/lang/String;
#10 = Utf8 ConstantValue
#11 = String #36 // 1
#12 = Utf8 name
#13 = Utf8 age
#14 = Utf8 I
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 this
#21 = Utf8 Lcn/app/wuzhi/MainHello;
#22 = Utf8 hello
#23 = Utf8 myQQ
#24 = Utf8 ()Ljava/lang/String;
#25 = Utf8 SourceFile
#26 = Utf8 MainHello.java
#27 = NameAndType #15:#16 // "<init>":()V
#28 = Class #37 // java/lang/System
#29 = NameAndType #38:#39 // out:Ljava/io/PrintStream;
#30 = Utf8 我是新的 MainHello
#31 = Class #40 // java/io/PrintStream
#32 = NameAndType #41:#42 // println:(Ljava/lang/String;)V
#33 = Utf8 123456
#34 = Utf8 cn/app/wuzhi/MainHello
#35 = Utf8 java/lang/Object
#36 = Utf8 1
#37 = Utf8 java/lang/System
#38 = Utf8 out
#39 = Utf8 Ljava/io/PrintStream;
#40 = Utf8 java/io/PrintStream
#41 = Utf8 println
#42 = Utf8 (Ljava/lang/String;)V
{
public cn.app.wuzhi.MainHello();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>
":()V
4: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/app/wuzhi/MainHello;
public void hello();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljav
a/io/PrintStream;
3: ldc #3 // String 我是新的 MainHello
5: invokevirtual #4 // Method java/io/PrintStream.prin
tln:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 14: 0
line 15: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcn/app/wuzhi/MainHello;
public java.lang.String myQQ();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: ldc #5 // String 123456
2: areturn
LineNumberTable:
line 21: 0
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this Lcn/app/wuzhi/MainHello;
}
SourceFile: "MainHello.java"
从Constant pool结构中,可以看到下标8-10,12-26,30,33-42,为Constant_Utf8_info类型,与打印结果吻合,完美。
章节一 完,这里就先写到常量结构,下章节详述其他结构体。
文章参考
https://blog.csdn.net/sinat_38259539/article/details/78248454