Java 类文件
所谓 Java 类文件,就是通常用 javac 编译器产生的 .class 文件。
这些文件具有严格定义的格式。
为了更好的理解 ASM,首先对 Java 类文件格式作一点简单的介绍。
Java 源文件经过 javac 编译器编译之后,将会生成对应的二进制文件(如下图所示)。
每个合法的 Java 类文件都具备精确的定义,而正是这种精确的定义,才使得 Java 虚拟机得以正确读取和解释所有的 Java 类文件。
Java 类文件是 8 位字节的二进制流。
数据项按顺序存储在 class 文件中,相邻的项之间没有间隔,这使得 class 文件变得紧凑,减少存储空间。
在 Java 类文件中包含了许多大小不同的项,由于每一项的结构都有严格规定,这使得 class 文件能够从头到尾被顺利地解析。下面让我们来看一下 Java 类文件的内部结构,以便对此有个大致的认识。
例子
例如,一个最简单的 Hello World 程序:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello world");
}
}
经过 javac 编译后,得到的类文件大致是:
从上图中可以看到,一个 Java 类文件大致可以归为 10 个项:
Magic:该项存放了一个 Java 类文件的魔数(magic number)和版本信息。一个 Java 类文件的前 4 个字节被称为它的魔数。每个正确的 Java 类文件都是以 0xCAFEBABE 开头的,这样保证了 Java 虚拟机能很轻松的分辨出 Java 文件和非 Java 文件。
Version:该项存放了 Java 类文件的版本信息,它对于一个 Java 文件具有重要的意义。因为 Java 技术一直在发展,所以类文件的格式也处在不断变化之中。类文件的版本信息让虚拟机知道如何去读取并处理该类文件。
Constant Pool:该项存放了类中各种文字字符串、类名、方法名和接口名称、final 变量以及对外部类的引用信息等常量。虚拟机必须为每一个被装载的类维护一个常量池,常量池中存储了相应类型所用到的所有类型、字段和方法的符号引用,因此它在 Java 的动态链接中起到了核心的作用。常量池的大小平均占到了整个类大小的 60% 左右。
Access_flag:该项指明了该文件中定义的是类还是接口(一个 class 文件中只能有一个类或接口),同时还指名了类或接口的访问标志,如 public,private, abstract 等信息。
This Class:指向表示该类全限定名称的字符串常量的指针。
Super Class:指向表示父类全限定名称的字符串常量的指针。
Interfaces:一个指针数组,存放了该类或父类实现的所有接口名称的字符串常量的指针。以上三项所指向的常量,特别是前两项,在我们用 ASM 从已有类派生新类时一般需要修改:将类名称改为子类名称;将父类改为派生前的类名称;如果有必要,增加新的实现接口。
Fields:该项对类或接口中声明的字段进行了细致的描述。需要注意的是,fields 列表中仅列出了本类或接口中的字段,并不包括从超类和父接口继承而来的字段。
Methods:该项对类或接口中声明的方法进行了细致的描述。例如方法的名称、参数和返回值类型等。需要注意的是,methods 列表里仅存放了本类或本接口中的方法,并不包括从超类和父接口继承而来的方法。使用 ASM 进行 AOP 编程,通常是通过调整 Method 中的指令来实现的。
Class attributes:该项存放了在该文件中类或接口所定义的属性的基本信息。
事实上,使用 ASM 动态生成类,不需要像早年的 class hacker 一样,熟知 class 文件的每一段,以及它们的功能、长度、偏移量以及编码方式。ASM 会给我们照顾好这一切的,我们只要告诉 ASM 要改动什么就可以了 —— 当然,我们首先得知道要改什么:对类文件格式了解的越多,我们就能更好地使用 ASM 这个利器。
Class
本章介绍如何使用核心ASM API生成和转换已编译的Java类。
它首先介绍了已编译的类,然后提供了许多说明性示例,介绍了相应的ASM接口,组件和生成和转换它们的工具。
在下一章中将介绍方法,注释和泛型的内容。
Structure
概览
编译类的整体结构非常简单。
实际上,与本地编译的应用程序不同,编译的类保留结构信息和源代码中几乎所有的符号。
实际上,一个已编译的类包含:
-
描述修饰符(例如公共或私有),名称,超类,接口和类的注释的部分。
-
此类中每个字段声明一个部分。
每个部分都描述了字段的修饰符,名称,类型和注释。
- 在该类中声明的每个方法和构造函数一节。
每个部分都描述了修饰符,名称,返回和参数类型以及方法的注释。
它还以Java字节码指令序列的形式包含该方法的已编译代码。
区别
但是,源类和编译后的类之间存在一些差异:
- 编译的类仅描述一个类,而源文件可以包含多个类。
例如,将描述一个内部类的类的源文件编译为两个类文件:一个用于主类,一个用于主类。 内部阶级。
但是,主类文件包含对其内部类的引用,方法内部定义的内部类包含对以下类的引用: 他们的封闭方法。
- 当然,已编译的类不包含注释,但可以包含可用于将其他信息与这些元素相关联的类,字段,方法和代码属性。
自从Java 5中引入了可用于相同目的的 annotations 以来,属性几乎变得无用。
- 编译的类不包含package和import section,因此所有类型名称都必须完全限定。
另一个非常重要的结构差异是,已编译的类包含一个常量池部分。
该池是一个包含所有出现在类中的数字,字符串和类型常量的数组。
这些常量仅在常量池部分中定义一次,并在类文件的所有其他部分中由它们的索引引用。
希望ASM隐藏与常量池相关的所有详细信息,因此您不必费心。
图2.1总结了已编译类的整体结构。
确切的结构在Java虚拟机规范的第4节中进行了描述。
另一个重要的区别是Java类型在编译类和源类中的表示方式有所不同。
下一节将解释它们在编译类中的表示形式。
内部名称
在许多情况下,类型被约束为类或接口类型。
例如,类的超类,由类实现的接口或方法引发的异常不能是原始类型或数组类型,而必须是类或接口类型。
这些类型在带有内部名称的已编译类中表示。
一个类的内部名称就是该类的完全限定名称,其中点用斜杠替换。
例如,字符串的内部名称是 java/lang/String。
类型描述符
内部名称仅用于约束为类或接口类型的类型。
在所有其他情况下,例如字段类型,Java类型都在带有类型描述符的已编译类中表示(参见图2.2)。
Java type | Type descriptor |
---|---|
boolean | Z |
char | C |
byte | B |
short | S |
int | I |
float | F |
long | J |
double | D |
Object | Ljava/lang/Object; |
int[] | [I |
Object[][] | [[Ljava/lang/Object; |
基本类型的描述符是单个字符:Z表示布尔值,C表示字符,B表示字节,S表示简短,I表示整数,F表示浮点数,J表示长整数,D表示双精度字符。
类类型的描述符是该类的内部名称,前面是L,然后是分号。
例如,String的类型描述符是 Ljava/lang/ String;
。
最后,数组类型的描述符是方括号,后跟数组元素类型的描述符。
方法描述符
方法描述符是类型描述符的列表,这些类型描述符在单个字符串中描述方法的参数类型和返回类型。
方法描述符以左括号开头,然后是每个形式参数的类型描述符,然后是右括号,然后是返回类型的类型描述符,如果该方法返回void,则返回V(方法描述符不包含方法名称或参数名称)。
- 图 2.3
源代码 | 描述符 |
---|---|
void m(int i, float f) | (IF)V |
int m(Object o) | (Ljava/lang/Object;)I |
int[] m(int i, String s) | (ILjava/lang/String;)[I |
Object m(int[] i) | ([I)Ljava/lang/Object; |
一旦知道了类型描述符的工作原理,就很容易理解方法描述符。
例如 (I)I
描述了一个采用一个类型为实参的方法 int,并返回一个int。
图2.3给出了几个方法描述符示例。
拓展学习
JVM class 文件信息
参考文档
https://asm.ow2.io/asm4-guide.pdf
https://www.ibm.com/developerworks/cn/java/j-lo-asm30/index.html