通过前面几节的讲解,我们已经基本了解了 class 文件的结构,每个字段代表什么含义,占多少字节,那么明确了 Class 文件的规则,接下来就可以读取 class 文件了,本节的代码均在项目的 classfile 包下。

Class 文件的结构

这里再次贴出 class 文件的结构描述:

  1. ClassFile {
  2. u4 magic; //魔数
  3. u2 minor_version; //次版本号
  4. u2 major_version; //主版本号
  5. u2 constant_pool_count; //常量池大小
  6. cp_info constant_pool[constant_pool_count-1]; //常量池
  7. u2 access_flags; //类访问标志,表明 class 文件定义的是类还是接口,访问级别是 public 还是 private,等
  8. u2 this_class; //
  9. u2 super_class; //
  10. u2 interfaces_count; //本类实现的接口数量
  11. u2 interfaces[interfaces_count]; //实现的接口,存放在数组中
  12. u2 fields_count; //本来中含有字段数
  13. field_info fields[fields_count]; //数组中存放这各个字段
  14. u2 methods_count; //本类中含有的方法数
  15. method_info methods[methods_count]; //数组中存放着各个方法
  16. u2 attributes_count; //本类中含有的属性数量;
  17. attribute_info attributes[attributes_count]; //数组中存放着各个属性
  18. }

ClassFile 类

根据上面的 class 结构类型,我们自己定义的类 ClassFile 也呼之欲出,每个字段和上述 class 结构几乎是一样的。类中的一个成员变量定义如下:

  1. public class ClassFile {
  2. int minorVersion;
  3. int majorVersion;
  4. ConstantPool constantPool;
  5. int accessFlags;
  6. int thisClass;
  7. int superClass;
  8. int[] interfaces;
  9. MemberInfo[] fields;
  10. MemberInfo[] methods;
  11. AttributeInfo[] attributes;
  12. public ClassFile(byte[] classData) {
  13. ClassReader reader = new ClassReader(classData);
  14. read(reader);
  15. }
  16. void read(ClassReader reader) {
  17. readAndCheckMagic(reader); //验证魔数
  18. readAndCheckVersion(reader); //校验版本
  19. constantPool = new ConstantPool(reader); //创建常量池
  20. accessFlags = reader.readUint16(); //获取类访问标志
  21. thisClass = reader.readUint16(); //
  22. superClass = reader.readUint16(); //
  23. interfaces = reader.readUint16s(); //
  24. fields = MemberInfo.readMembers(reader, constantPool); //
  25. methods = MemberInfo.readMembers(reader, constantPool); //
  26. attributes = AttributeInfo.readAttributes(reader, constantPool); //
  27. }
  28. }

可以很清晰的看到,我们定义的 ClassFile 文件,可以看到成员变量和 JVM 中关于 class 文件的描述是一致的,只不过这里为了方便编码,统一用 int 类型来保存 u1,u2 和 u4 类型的值.

Class 文件字节码读取辅助类

接下来要解决的问题是每个字段所占的字节数不同,所以这里我们需要若干方法根据字节数读取相应的字节,所有有创建了一个ClassReader类,并令该类持有 class 字节码,并且在该类中保存一个 index,表明现在是从哪个字节开始读.并且提供了读取 1 字节,2 字节,4 字节,8 字节等方法,来满足ClassFile类中个字段对应的字节数的需求。

  1. public class ClassReader {
  2. byte[] data;
  3. int index = 0;
  4. public ClassReader(byte[] data) {
  5. this.data = data;
  6. }
  7. // u1
  8. public byte readUint8() {
  9. byte res = data[index++];
  10. return res;
  11. }
  12. // u2 这里是读取一个无符号的 16 位整,java 中没有,只能用 int 来代替吧;
  13. public int readUint16() {
  14. byte[] res = new byte[2];
  15. res[0] = data[index++];
  16. res[1] = data[index++];
  17. return ByteUtils.bytesToU16(res);
  18. }
  19. // u4
  20. public byte[] readUint32() {
  21. byte[] res = new byte[4];
  22. res[0] = data[index++];
  23. res[1] = data[index++];
  24. res[2] = data[index++];
  25. res[3] = data[index++];
  26. // return ByteUtils.bytesToU32(res); //如果需要转换的话,自行调用 ByteUtils 中的方法;
  27. return res;
  28. }
  29. public byte[] readUint64() {
  30. byte[] res = new byte[8];
  31. res[0] = data[index++];
  32. res[1] = data[index++];
  33. res[2] = data[index++];
  34. res[3] = data[index++];
  35. res[4] = data[index++];
  36. res[5] = data[index++];
  37. res[6] = data[index++];
  38. res[7] = data[index++];
  39. return res;
  40. }
  41. public int[] readUint16s() {
  42. int n = readUint16();
  43. int[] data = new int[n];
  44. for (int i = 0; i < n; i++) {
  45. data[i] = readUint16();
  46. }
  47. return data;
  48. }
  49. public byte[] readBytes(int n) {
  50. byte[] res = new byte[n];
  51. for (int i = 0; i < n; i++) {
  52. res[i] = data[index++];
  53. }
  54. return res;
  55. }
  56. }

在有了 ClassReader 这样的工具类之后,我们就可以在 ClassFile 中根据不同的字段,使用不同的 ClassReader#readXXX() 方法,来初始化成员变量,这里定义了一个 ClassFile#read() 方法来完成 ClassFile 类的初始化成员变量的任务。

简单字段的实现

对于 ClassFile 的字段,除了常量和属性两个区域,其它的字段都可以很容易根据其占用的字节长度读出来,这对于编码来说并没有什么难度。在 ClassFile#read 方法中实现了所有字段的读取。

  1. void read(ClassReader reader) {
  2. readAndCheckMagic(reader);
  3. readAndCheckVersion(reader);
  4. constantPool = new ConstantPool(reader);
  5. accessFlags = reader.readUint16();
  6. thisClass = reader.readUint16();
  7. superClass = reader.readUint16();
  8. interfaces = reader.readUint16s();
  9. fields = MemberInfo.readMembers(reader, constantPool);
  10. methods = MemberInfo.readMembers(reader, constantPool);
  11. attributes = AttributeInfo.readAttributes(reader, constantPool);
  12. }

但是对于常量和属性,因为其各自又包含了许多种类,所以需要针对不同的常量,不同的属性进行不同的读取。接下来分别介绍如何实现常量和属性的读取。

常量的实现

这里需要定义一个常量的抽象类 ConstantInfo,表示一个常量 item,具体的常量由其子类实现,这里对外提供一个统一的接口来根据不同的 tag,创建不同的具体常量实现类,以完成常量池的初始化。

  1. private static ConstantInfo create(int tag, ConstantPool constantPool) {
  2. switch (tag) {
  3. case CONSTANT_Integer:
  4. return new ConstantIntegerInfo();
  5. case CONSTANT_Float:
  6. return new ConstantFloatInfo();
  7. case CONSTANT_Long:
  8. return new ConstantLongInfo();
  9. case CONSTANT_Double:
  10. return new ConstantDoubleInfo();
  11. case CONSTANT_Utf8:
  12. return new ConstantUtf8Info();
  13. case CONSTANT_String:
  14. return new ConstantStringInfo(constantPool);
  15. case CONSTANT_Class:
  16. return new ConstantClassInfo(constantPool);
  17. case CONSTANT_Fieldref:
  18. return new ConstantMemberRefInfo(constantPool);
  19. case CONSTANT_Methodref:
  20. return new ConstantMemberRefInfo(constantPool);
  21. case CONSTANT_InterfaceMethodref:
  22. return new ConstantMemberRefInfo(constantPool);
  23. case CONSTANT_NameAndType:
  24. return new ConstantNameAndTypeInfo();
  25. // TODO: 2017/5/3 0003 下面三个类还未编码;
  26. case CONSTANT_MethodType:
  27. return new ConstantMethodTypeInfo();
  28. case CONSTANT_MethodHandle:
  29. return new ConstantMethodHandleInfo();
  30. case CONSTANT_InvokeDynamic:
  31. return new ConstantInvokeDynamicInfo();
  32. default:
  33. throw new RuntimeException("java.lang.ClassFormatError: constant pool tag!");
  34. }
  35. }

并且提供一个抽象方法,供子类实现,因为每种常量所占的字节数并不相同。

  1. abstract void readInfo(ClassReader reader)

而对于各自具体的常量,需要根据各自常量的结构来读取,其结构已经在分析class文件-常量池中进行了详细的介绍。具体实现请参照项目源码

常量池的实现

有了上面各个常量的具体实现,那么接下来我们就可以构建常量池了。常量池其实就是本 class 文件中所有常量的集合。
因为常量池是根据索引来访问的,因此我们也很自然的想到用数组来表示常量池,数组类型是上面定义的常量类型,注意索引从 1 开始,0 是无效索引。常量池的初始化时在构造方法中,通过 ConstantInfo 提供的 readConstantInfo 静态方法,读取一字节 tag,根据 tag 创建不同的常量实现类,并添加到常量池数组中。

  1. public class RuntimeConstantPool {
  2. ConstantInfo[] infos; //保存类文件常量池中的所有常量,常量分为多种类型,基本类型都有对应的常量,以及字符串等;(简言之,这就是常量池的抽象)
  3. int constantPoolCount; //class 文件中常量池中的常量数量
  4. public ConstantPool(ClassReader reader) {
  5. /*读出常量池的大小;接下来根据这个大小,生成常量信息数组;
  6. 注意:
  7. 1. 表头给出的常量池大小比实际大 1,所以这样的话,虽然可能生成了这么大的,但是 0 不使用,直接从 1 开始;
  8. 2. 有效的常量池索引是 1~n–1。0 是无效索引,表示不指向任何常量
  9. 3. CONSTANT_Long_info 和 CONSTANT_Double_info 各占两个位置。
  10. 也就是说,如果常量池中存在这两种常量,实际的常量数量比 n–1 还要少,而且 1~n–1 的某些数也会变成无效索引。
  11. */
  12. constantPoolCount = reader.readUint16();
  13. infos = new ConstantInfo[constantPoolCount];
  14. for (int i = 1; i < constantPoolCount; i++) {
  15. infos[i] = ConstantInfo.readConstantInfo(reader, this);
  16. if ((infos[i] instanceof ConstantLongInfo) || (infos[i] instanceof ConstantDoubleInfo)) {
  17. i++;
  18. }
  19. }
  20. }
  21. ......
  22. }

属性的实现

对于属性的编码,和常量池的编码思路是相似的,其实这里称为“属性池”更为贴切。因为他是各种属性的集合。而 class 文件本身,方法表集合和字段表集合中均持有属性表。
同样,提供一个抽象类来表示一个属性,其定义如下:

  1. public abstract class AttributeInfo {
  2. abstract void readInfo(ClassReader reader);
  3. //读取单个属性
  4. private static AttributeInfo readAttribute(ClassReader reader, ConstantPool constantPool) {
  5. int attrNameIndex = reader.readUint16();
  6. String attrName = constantPool.getUtf8(attrNameIndex);
  7. int attrLen = ByteUtils.byteToInt32(reader.readUint32());
  8. AttributeInfo attrInfo = create(attrName, attrLen, constantPool);
  9. attrInfo.readInfo(reader);
  10. return attrInfo;
  11. }
  12. //读取属性表;这个和 ConstantPool 中的方法类似,一般都是一下全部读取出来,不会只读一个
  13. public static AttributeInfo[] readAttributes(ClassReader reader, ConstantPool constantPool) {
  14. int attributesCount = reader.readUint16();
  15. AttributeInfo[] attributes = new AttributeInfo[attributesCount];
  16. for (int i = 0; i < attributesCount; i++) {
  17. attributes[i] = readAttribute(reader, constantPool);
  18. }
  19. return attributes;
  20. }
  21. //Java 虚拟机规范预定义了 23 种属性,先解析其中的 8 种
  22. /*
  23. 23 种预定义属性可以分为三组。
  24. 第一组属性是实现 Java 虚拟机所必需的,共有 5 种;
  25. 第二组属性是 Java 类库所必需的,共有 12 种;
  26. 第三组属性主要提供给工具使用,共有 6 种。第三组属性是可选的,也就是说可以不出现在 class 文件中。
  27. (如果 class 文件中存在第三组属性,Java 虚拟机实现或者 Java 类库也是可以利用它们的,比如使用 LineNumberTable 属性在异常堆栈中显示行号。)
  28. */
  29. private static AttributeInfo create(String attrName, int attrLen, ConstantPool constantPool) {
  30. if (attrName.equals("Code")) {
  31. return new CodeAttribute(constantPool);
  32. }else if (attrName.equals("ConstantValue")){
  33. return new ConstantValueAttribute();
  34. }else if (attrName.equals("Deprecated")){
  35. return new DeprecatedAttribute();
  36. }else if (attrName.equals("Exceptions")){
  37. return new ExceptionsAttribute();
  38. }else if (attrName.equals("LineNumberTable")){
  39. return new LineNumberTableAttribute();
  40. }else if (attrName.equals("LocalVariableTable")){
  41. return new LocalVariableTableAttribute();
  42. }else if (attrName.equals("SourceFile")){
  43. return new SourceFileAttribute(constantPool);
  44. }else if (attrName.equals("Synthetic")){
  45. return new SyntheticAttribute();
  46. } else {
  47. return new UnparsedAttribute(attrName, attrLen);
  48. }
  49. }
  50. }

其内部定义了抽象方法 readInfo,供各具体的属性类去读取相应的数据。而对外,提供了一个 readAttributes 的方法,来返回当前 方法表集合或者字段表集合中的的属性集合。与常量池不同的是:常量是根据不同的 tag(代表一个整数)来区分不同的常量,而属性是根据不同的 name(字符串)来区分不同的属性,所以创建属性的方法 AttributeInfo#create 方法。
而对于各自具体的属性,需要根据各自属性的结构来读取,其结构已经在分析class文件-属性表中进行了详细的介绍。具体实现请参照项目源码