Java 类文件

所谓 Java 类文件,就是通常用 javac 编译器产生的 .class 文件。

这些文件具有严格定义的格式。

为了更好的理解 ASM,首先对 Java 类文件格式作一点简单的介绍。

Java 源文件经过 javac 编译器编译之后,将会生成对应的二进制文件(如下图所示)。

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 编译后,得到的类文件大致是:

class files

从上图中可以看到,一个 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节中进行了描述。

image

另一个重要的区别是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