Java虚拟机字节码:从入门到实战
上QQ阅读APP看书,第一时间看更新

解析字段表

同一个Class文件中,不会存在两个相同的字段。相同指定的是字段名与类型描述符都相同。字段结构与Class文件结构一样,都有访问标志项,但两者的访问标志项,在访问权限和属性上有些区别。参照《Java虚拟机规范》,字段中的访问权限和属性标志如表2-41所示。

表2-41 字段访问权限与属性标志

字段的结构如表2-42所示。

表2-42 字段的结构

其中,access_flags是字段的访问标志,name_index是字段名称,descriptor_index是字段的类型描述符。字段结构与方法结构、Class文件结构都有属性表attributes,属性表的属性个数可以是0个或多个。属性的通用结构如表2-43所示。

表2-43 属性的通用结构

关于属性,我们先了解属性的通用结构,实现属性的初步解析,让字段解析器能够完成字段的解析工作,至于属性info是什么暂时先不关心。

创建字段结构对应的Java类FieldInfo,如代码清单2-44所示。

代码清单2-44 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,如代码清单2-45所示。

代码清单2-45 AttributeInfo类

public class AttributeInfo {
    private U2 attribute_name_index;
    private U4 attribute_length;
    private byte[] info;
}

创建字段表解析器FieldHandler,实现字段表的解析。字段结构的属性表的解析工作也由字段表解析器完成。解析流程如下:

1、先从class文件字节缓存中读取到字段总数,根据字段总数创建字段表;

2、循环解析出每个字段;

3、解析字段的属性表时,先解析获取到属性总数,再根据属性总数创建属性表;

4、使用通用属性结构循环解析出字段的每个属性;

5、解析属性时,先解析出attribute_name_index,再解析attribute_length获取属性info的长度,根据长度读取指定长度的字节数据存放到属性的info字段。

字段表解析器的实现,如代码清单2-46所示。

代码清单2-46 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,如代码清单2-47所示。

代码清单2-47 access_flags转字符串工具类

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,再编写单元测试。编写单元测试用于验证字段解析器是否能够正确完成字段表地解析。编写单元测试要求将解析后的所有字段的名称、类型、以及字段的访问标志转为字符串打印出来,以验证结果是否正确。字段表解析器单元测试如代码清单2-48所示。

代码清单2-48 字段表解析器单元测试

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();
        }
    }

}

单元测试结果输出如图2.9所示。

图2.9 字段表解析器单元测试

从结果输出可以看出,该class有三个字段,访问权限都是private的,字段名分别是a、b、c,并且类型描述符都是“I”,即字段的类型都是整型。