📚《深入浅出JVM字节码》
《Java虚拟机字节码:从入门到实战》的开源版本。作者通过自己的实战经验,整合出一套适合新手的高效学习教程。归纳并提炼知识点,制定合理路线,帮助读者更快掌握核心技术。
在同一个Class文件中,不允许存在两个相同的字段,相同指的是字段名与类型描述符都相同。
字段结构与Class文件结构一样,都有访问标志项,但两者的访问标志项在访问权限和属性上有些区别。
字段中的访问权限和属性标志
标志名 | 十六进制值 | 说明 |
---|---|---|
ACC_PUBLIC | 0x 00 01 | 声明为public |
ACC_PRIVATE | 0x 00 02 | 声明为private |
ACC_PROTECTED | 0x 00 04 | 声明为protected |
ACC_STATIC | 0x 00 08 | 声明为static |
ACC_FINAL | 0x 00 10 | 声明为final |
ACC_VOLATILE | 0x 00 40 | 声明为volatile |
ACC_TRANSIENT | 0x 00 80 | 声明为transient |
ACC_SYNTHTIC | 0x 10 00 | 标志该字段由编译器产生,不在源码中 |
ACC_ENUM | 0x 40 00 | 声明为枚举类型的成员 |
字段的结构如下表所示。
字段名 | 类型 | 说明 |
---|---|---|
access_flags | u2 | 访问标志 |
name_index | u2 | 字段名,指向常量池中某个常量的索引 |
descriptor_index | u2 | 类型描述符,指向常量池中某个常量的索引 |
attributes_count | u2 | 属性总数 |
attributes | attribute_info | 属性 |
其中,access_flags是字段的访问标志,name_index是字段名称常量的索引,descriptor_index是字段的类型描述符常量索引。字段结构与方法结构、Class文件结构都有属性表attributes,属性表的属性个数可以是0个或多个。
属性的通用结构如下表所示。
字段名 | 类型 | 说明 |
---|---|---|
attribute_name_index | u2 | 属性名称,指向常量池某个常量的索引 |
attribute_length | u4 | 属性长度,属性info的字节总数 |
info | u1 | 属性信息,不同的属性不同的解析 |
提示:关于属性,我们先了解属性的通用结构,实现属性的初步解析,让字段解析器能够完成字段的解析工作,至于属性info是什么我们暂时先不关心。
首先创建字段结构对应的Java类FieldInfo,代码如下。
public class FieldInfo {
private U2 access_flags;
private U2 name_index;
private U2 descriptor_index;
private U2 attributes_count;
private AttributeInfo[] attributes;
}
接着创建属性结构对应的Java类AttributeInfo,代码如下。
public class AttributeInfo {
private U2 attribute_name_index;
private U4 attribute_length;
private byte[] info;
}
最后创建字段表解析器FieldHandler,实现字段表的解析。字段结构的属性表的解析工作也由字段表解析器完成,解析流程如下:
字段表解析器的实现代码如下。
public class FieldHandler implements BaseByteCodeHandler {
@Override
public int order() {
// 排在接口解析器的后面
return 6;
}
@Override
public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception {
// 读取字段总数
classFile.setFields_count(new U2(codeBuf.get(), codeBuf.get()));
int len = classFile.getFields_count().toInt();
if (len == 0) {
return;
}
// 创建字段表
FieldInfo[] fieldInfos = new FieldInfo[len];
classFile.setFields(fieldInfos);
// 循环解析出每个字段
for (int i = 0; i < fieldInfos.length; i++) {
// 解析字段
fieldInfos[i] = new FieldInfo();
// 读取字段的访问标志
fieldInfos[i].setAccess_flags(new U2(codeBuf.get(), codeBuf.get()));
// 读取字段名称
fieldInfos[i].setName_index(new U2(codeBuf.get(), codeBuf.get()));
// 读取字段类型描述符索引
fieldInfos[i].setDescriptor_index(new U2(codeBuf.get(), codeBuf.get()));
// 读取属性总数
fieldInfos[i].setAttributes_count(new U2(codeBuf.get(), codeBuf.get()));
// 获取字段的属性总数
int attr_len = fieldInfos[i].getAttributes_count().toInt();
if (attr_len == 0) {
continue;
}
// 创建字段的属性表
fieldInfos[i].setAttributes(new AttributeInfo[attr_len]);
// 循环解析出每个属性,先使用通用属性结构解析每个属性
for (int j = 0; j < attr_len; j++) {
// 解析字段的属性
fieldInfos[i].getAttributes()[j]
.setAttribute_name_index(new U2(codeBuf.get(),codeBuf.get()));
// 获取属性info的长度
U4 attr_info_len = new U4(codeBuf.get(), codeBuf.get(), codeBuf.get(), codeBuf.get());
fieldInfos[i].getAttributes()[j].setAttribute_length(attr_info_len);
// 解析info
byte[] info = new byte[attr_info_len.toInt()];
codeBuf.get(info, 0, attr_info_len.toInt());
fieldInfos[i].getAttributes()[j].setInfo(info);
}
}
}
}
为验证解析结果,我们还需要编写将字段的访问标志access_flags转为字符串输出的工具类FieldAccessFlagUtils ,代码如下。
public class FieldAccessFlagUtils {
private static final Map<Integer, String> fieldAccessFlagMap = new HashMap<>();
static {
fieldAccessFlagMap.put(0x0001, "public");
fieldAccessFlagMap.put(0x0002, "private");
fieldAccessFlagMap.put(0x0004, "protected");
fieldAccessFlagMap.put(0x0008, "static");
fieldAccessFlagMap.put(0x0010, "final");
fieldAccessFlagMap.put(0x0040, "volatile");
fieldAccessFlagMap.put(0x0080, "transient");
fieldAccessFlagMap.put(0x1000, "synthtic");
fieldAccessFlagMap.put(0x4000, "enum");
}
/**
* 获取16进制对应的访问标志和属性字符串表示 (仅用于类的访问标志)
*
* @param flag 字段的访问标志
* @return
*/
public static String toFieldAccessFlagsString(U2 flag) {
final int flagVlaue = flag.toInt();
StringBuilder flagBuild = new StringBuilder();
fieldAccessFlagMap.keySet()
.forEach(key -> {
if ((flagVlaue & key) == key) {
flagBuild.append(fieldAccessFlagMap.get(key)).append(",");
}
});
return flagBuild.length() > 0 && flagBuild.charAt(flagBuild.length() - 1) == ',' ?
flagBuild.substring(0, flagBuild.length() - 1) : flagBuild.toString();
}
}
字段表解析器编写完成后,我们先将字段解析器注册到ClassFileAnalysiser,再编写单元测试。要求单元测试需要将解析后的所有字段的名称、类型、以及字段的访问标志转为字符串打印出来,以验证结果是否正确。
字段表解析器单元测试代码如下。
public class FieldHandlerTest {
private static String getName(U2 name_index, ClassFile classFile) {
CONSTANT_Utf8_info name_info = (CONSTANT_Utf8_info)
classFile.getConstant_pool()[name_index.toInt() - 1];
return name_info.toString();
}
@Test
public void testFieldHandlerHandler() throws Exception {
ByteBuffer codeBuf = ClassFileAnalysisMain.readFile("Builder.class");
ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf);
System.out.println("字段总数:" + classFile.getFields_count().toInt());
System.out.println();
FieldInfo[] fieldInfos = classFile.getFields();
// 遍历字段表
for (FieldInfo fieldInfo : fieldInfos) {
System.out.println("访问标志和属性:" + FieldAccessFlagUtils
.toFieldAccessFlagsString(fieldInfo.getAccess_flags()));
System.out.println("字段名:"
+ getName(fieldInfo.getName_index(), classFile));
System.out.println("字段的类型描述符:"
+ getName(fieldInfo.getDescriptor_index(), classFile));
System.out.println("属性总数:"
+ fieldInfo.getAttributes_count().toInt());
System.out.println();
}
}
}
单元测试结果如下图所示。
从输出的结果可以看出,该class有三个字段,访问权限都是private的,字段名分别是a、b、c,并且类型描述符都是“I”,即字段的类型都是整型。
发布于:2021 年 07 月 24 日
作者: 吴就业
链接: https://github.com/wujiuye/JVMByteCodeGitBook
来源: Github Pages 开源电子书《深入浅出JVM字节码》(《Java虚拟机字节码从入门到实战》的第二版),未经作者许可,禁止转载!
📚目录
订阅
订阅新文章发布通知吧,不错过精彩内容!
输入邮箱,提交后我们会给您发送一封邮件,您需点击邮件中的链接完成订阅设置。