当前位置: 移动技术网 > IT编程>开发语言>Java > Java Class 解析器实现方法示例

Java Class 解析器实现方法示例

2019年07月19日  | 移动技术网IT编程  | 我要评论

最近在写一个私人项目,名字叫做classanalyzer,classanalyzer的目的是能让我们对java class文件的设计与结构能够有一个深入的理解。主体框架与基本功能已经完成,还有一些细节功能日后再增加。实际上jdk已经提供了命令行工具javap来反编译class文件,但本篇文章将阐明我实现解析器的思路。

class文件

作为类或者接口信息的载体,每个class文件都完整的定义了一个类。为了使java程序可以“编写一次,处处运行”,对class文件进行了严格的规定。构成class文件的基本数据单位是字节,这些字节之间不存在任何分隔符,这使得整个class文件中存储的内容几乎全部是程序运行的必要数据,单个字节无法表示的数据由多个连续的字节来表示。

根据java虚拟机规范,class文件采用一种类似于c语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。java虚拟机规范定义了u1、u2、u4和u8来分别表示1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者是字符串。表是由多个无符号数或者其它表作为数据项构成的复合数据类型,表用于描述有层次关系的复合结构的数据,因此整个class文件本质上就是一张表。在classanalyzer中,byte、short、int和long分别对应u1、u2、u4和u8数据类型,class文件被描述为如下java类。

public class classfile {
 public u4 magic;       // magic
 public u2 minorversion;      // minor_version
 public u2 majorversion;      // major_version
 public u2 constantpoolcount;    // constant_pool_count
 public constantpoolinfo[] cpinfo;   // cp_info
 public u2 accessflags;      // access_flags
 public u2 thisclass;      // this_class
 public u2 superclass;      // super_class
 public u2 interfacescount;     // interfaces_count
 public u2[] interfaces;      // interfaces
 public u2 fieldscount;      // fields_count
 public fieldinfo[] fields;     // fields
 public u2 methodscount;      // methods_count
 public methodinfo[] methods;    // methods
 public u2 attributescount;     // attributes_count
 public basicattributeinfo[] attributes;  // attributes
}

如何解析

组成class文件的各个数据项中,例如魔数、class文件的版本等数据项、访问标志、类索引、父类索引,它们在每个class文件中都占用固定数量的字节,在解析时只需要读取相应数量的字节。除此之外,需要灵活处理的主要包括4部分:常量池、字段表集合、方法表集合和属性表集合。字段和方法都可以具备自己的属性,class本身也有相应的属性,因此,在解析字段表集合和方法表集合的同时也包含了属性表的解析。

常量池占据了class文件很大一部分的数据,用于存储所有的常量信息,包括数字和字符串常量、类名、接口名、字段名和方法名等。java虚拟机规范定义了多种常量类型,每一种常量类型都有自己的结构。常量池本身是一个表,在解析时有几点需要注意。

每个常量类型都通过一个u1类型的tag来标识。

表头给出的常量池大小(constantpoolcount)比实际大1,例如,如果constantpoolcount等于47,那么常量池中有46项常量。

常量池的索引范围从1开始,例如,如果constantpoolcount等于47,那么常量池的索引范围为1~46。设计者将第0项空出来的目的是用于表达“不引用任何一个常量池项目”。

constant_utf8_info型常量的结构中包含u1类型的tag、u2类型的length和由length个u1类型组成的bytes,这length字节的连续数据是一个使用mutf-8(modified utf-8)编码的字符串。mutf-8与utf-8并不兼容,主要区别有两点:一是null字符会被编码成2字节(0xc0和0x80);二是补充字符是按照utf-16拆分为代理对分别编码的,相关细节可以看这里(变种utf-8)。

属性表用于描述某些场景专有的信息,class文件、字段表和方法表都有相应的属性表集合。java虚拟机规范定义了多种属性,classanalyzer目前实现了对常用属性的解析。和常量类型的数据项不同,属性并没有一个tag来标识属性的类型,但是每个属性都包含有一个u2类型的attribute_name_index,attribute_name_index指向常量池中的一个constant_utf8_info类型的常量,该常量包含着属性的名称。在解析属性时,classanalyzer正是通过attribute_name_index指向的常量对应的属性名称来得知属性的类型。

字段表用于描述类或者接口中声明的变量,字段包括类级变量以及实例级变量。字段表的结构包含一个u2类型的access_flags、一个u2类型的name_index、一个u2类型的descriptor_index、一个u2类型的attributes_count和attributes_count个attribute_info类型的attributes。我们已经介绍了属性表的解析,attributes的解析方式与属性表的解析方式一致。

class的文件方法表采用了和字段表相同的存储格式,只是access_flags对应的含义有所不同。方法表包含着一个重要的属性:code属性。code属性存储了java代码编译成的字节码指令,在classanalyzer中,code对应的java类如下所示(仅列出了类属性)

public class code extends basicattributeinfo {
 private short maxstack;
 private short maxlocals;
 private long codelength;
 private byte[] code;
 private short exceptiontablelength;
 private exceptioninfo[] exceptiontable;
 private short attributescount;
 private basicattributeinfo[] attributes;
 ...
 private class exceptioninfo {
  public short startpc;
  public short endpc;
  public short handlerpc;
  public short catchtype;
   ...
 }
}

在code属性中,codelength和code分别用于存储字节码长度和字节码指令,每条指令即一个字节(u1类型)。在虚拟机执行时,通过读取code中的一个个字节码,并将字节码翻译成相应的指令。另外,虽然codelength是一个u4类型的值,但是实际上一个方法不允许超过65535条字节码指令。

代码实现

classanalyzer的源码已放在了github上。在classanalyzer的readme中,我以一个类的class文件为例,对该class文件的每个字节进行了分析,希望对大家的理解有所帮助。

参考

深入理解java虚拟机

结束语:以上就是本文关于如何实现一个java class 解析器的全部内容,希望对大家有所帮助。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网