[动态代理三部曲:中] - 从动态代理,看Class文件结构定义

908 阅读7分钟

前言

这篇内容是上一篇[动态代理三部曲:上] - 动态代理是如何"坑掉了"我4500块钱的补充,进一步分析篇。 建议二者结合食用,醇香绵软,入口即化。

好了,不扯淡了,开始...

正文

2、Class 文件的格式

这里为啥是2开头呢?因为上篇文章是1

这部分内容不知道各位小伙伴是怎么感觉的。最开始学习的时候,我是一头雾水,不知道如何下手。当一步步结合反射、JVM内存模型,类加载机制后。再回过头来就会发现一起豁然开朗。

此篇内容的开始,让我们根据我们demo中所用的类:RentHouseProcessorHandler来分析这个问题。
如果我们用十六进制编辑器(比如:Sublime)打开这个RentHouseProcessorHandler.class文件:

十六进制的Class文件

说实话这一行行的文字,最开始我是拒绝的。哦,上帝,为什么要让我看这些鬼东西...其实如果我们静下心来,想当年高中时代学习数学,物理公式那样去认真的对待它。就会发现它不过就是:一堆人为赋予了特别含义的符号而已。

在我们准备读懂这些十六进制文字时,先让我们看一幅《Java虚拟机规范(Java SE 7)》对class文件的定义:

《Java虚拟机规范(Java SE 7)》


2.1、Class文件的规范结构

2.1.1、标准结构

上图的内容,其实非常的通俗易懂,不要因为是不常见的英文就抵触它们。让我们尝试着去翻译它们: 1、魔数;2、次版本号;3、主版本号;4、常量池数量;5、常量池;6、权限标识;7、此类;8、父类;9、接口数目;10、接口;11、变量数目;12、变量;13、方法数目;14、方法....

Class结构

其实是不是发现了什么,这不是就一个类应该存在的东西么?没错啊,Class文件的结构就是固定了我们编写的Class类所存放的规则而已。最开始的我,以为是深奥,没敢去了解他们。当我踌躇满志,鼓足勇气去准备好好大干一场的时候,才发现它太简单了...就是一些规则,仅此而已。

2.1.2、特别注意的结构:表

虽然只是一些规则,但规则之中,总会有一些特别需要我们去注意的地方:比如cp_info这个类型。在《深入理解Java虚拟机》中,作者把以_info结尾的类型称之为“表”。这里让我们也沿用这种表达方式。说白了,它就是拥有多级关系的类型。

cp_info 表示常量池(常量池:首先它和方法区中运行时常量池不是同一个内容。这里的常量池存放了字面量和符号引用)。


符号引用:

符号引用:

  • 类和接口的全局限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

这里符号引用的作用,我们想先一个问题。CPU执行程序的时候,实际上是去寻找对应指令的内存地址。但是我们的Class文件是先被编译出来的,但是此时还没有被JVM加载到内存,所以肯定是不可能存在内存地址这一说的。因此我们的Class文件需要一些标识,让JVM加载内容的时候从常量池中获取到对应的符号引用,然后在映射到具体的内存地址上。


放到常量池的中数据项在《Java虚拟机规范(Java SE 7)》中一共有14个常量,每一种常量都是一个“表”,并且每种常量都用一个公共的tag来表示是哪种类型的常量。具体内容如下图:

表类型.png

这里让我们先解读一下这个常量池:让我们跳过u4的魔数、u2的此版本、u2的主版本。直接来看constant_pool_count。跳过对应的内容,那么我们的constant_pool_count就对应十六进制的20,对应十进制的32,也就是说常量池中有32个内容?实际不是的,因为设计者将第0个位置空出来另做打算。所以我们的常量池只有31个内容。

constant_pool_count

我们可以通过javap命令证实这个问题。

javap

接下来的一个字节:0a,翻译成十进制就是10,对应我们表中的CONSTANT_Methodref_info,而接下来的四个字节。分别代表索引3,20。这里的索引代表什么意思呢?注意理解下图中标红的地方:

常量池只有31个内容

接下来就不逐个解读这些内容了,因为它就是一个对应的过程。如果小伙伴们有兴趣可以自行去尝试解读一番哦。推荐一个工具JavaClassViewer,可以比较方便的查看这些内容:

JavaClassViewer-左视图

JavaClassViewer-右视图


常量池结束之后,便是我们正常的变量,方法的信息。而这里我们需要了解一个全新的概念:描述符。 对于我们来说一个变量、方法在java源码里是什么样子我们很清楚。但是它们在class文件里是什么样子的呢?这个样子其实就被称之为:描述符。

上文谈动态代理的时候,我们了解到了ProxyGenerator.generateProxyClass(proxyName,interfaces, accessFlags);方法中,通过:

dout.writeInt(0xCAFEBABE);
MethodInfo minfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V",ACC_PUBLIC);    

等方法,构建了我们的$Proxy0所需要的class结构,是不是和我们javap出来的内容很类似?接下来让我们走进描述符。

2.2、变量、方法的描述符

经过上述内容的铺垫,0xCAFEBABE是什么意思,应该无需多言了。而<init>是构造方法的意思。再加上(Ljava/lang/reflect/InvocationHandler;)V以及ACC_PUBLIC就可以表示为InvocationHandler的public的构造方法,其中V表示无返回值。

  • <init>:对象构造器方法。
  • <clinit>:类构造器方法。

这里的内容就被称之为方法的描述符,让我们简单的看一些图,加深这方面内容。

类型描述符

基本类型和void在描述符中都有一个大写字符和他们对应; 那么引用类型的描述符,又是什么样子的呢?

“L” + 类型的全限定名 + “;”

例如下图中的:Ljava/lang/Object;就是表示这是一个Object类型。

方法对应的描述符

而方法描述符的规则也很简单,上图中,总结出来就是一句话:

(参数类型1参数类型2参数类型3 ...)返回值类型

例如上图中的:int[] m(int i,String s)转换为描述符:(ILjava/lang/String;)[I

不知道截了这么多图,大家对描述符有没有比较明确的认识。说白了我们我们ProxyGenerator.generateProxyClass(proxyName,interfaces, accessFlags);中所write的内容就是具体方法的描述符。

然后通过DataOutputStream转成byte数组,那么就是我们Class文件所固定的内容了。因此,此时我们的Class文件就已经构建完毕,接下来所需要的就是将其加载到内存中,供我们使用。

2.3、收尾

到这准备结束class文件结构的内容。不知道小伙伴们是否有收获。因为篇幅是在有限,有些内容又不是一句话俩句话可以描述清楚的。所以有些内容一带而过,实在抱歉。具体细节内容,大家可以参考《深入理解Java虚拟机》。

希望大家可以谅解。

我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,已经我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

个人公众号:IT面试填坑小分队