深入解析Java编译器:源码剖析与实例详解(txt+pdf+epub+mobi电子书下载)


发布时间:2020-05-18 01:48:43

点击下载

作者:马智

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

深入解析Java编译器:源码剖析与实例详解

深入解析Java编译器:源码剖析与实例详解试读:

前言

为什么要写这本书?

Java是一门流行多年的高级编程语言,相关的就业岗位很多,但是最近几年却出现了用人单位招不到人,大量研发人员找不到工作的尴尬局面。根本原因还是岗位对技术的要求高,不但要会应用,更要懂其内部的运行原理。对于想要深入研究Java的从业者来说,目前国内市场上还没有一本剖析Java编译器(Javac)源码的书籍,也没有一本剖析工业级编译器源码的书籍,这正是本书要填补的市场空白。

本书围绕市面上的主流编译器Javac进行源代码剖析,详细介绍了词法分析、语法分析、语义分析及代码生成等各个阶段的具体实现。另外,本书有大量的配图和实例,以便读者能更好地理解书中的要点。本书有何特色

1.内容丰富,讲解详细

本书对Java编译器的词法分析、语法分析、语义分析及代码生成等各个阶段的源代码实现做了详细介绍,可以帮助读者系统地掌握Java编译器的实现原理。

2.原理分析与实例并重

本书对Java编译器各个编译阶段的源代码实现都进行了重点介绍,同时也简单介绍了一些编译器的基本原理,并给出了大量的配图和实例,可以让读者能真正掌握Java编译器的具体实现。

3.分析工业级的编译器源码实现

本书分析的编译器Javac是一个工业级的编译器,它是大部分Java开发人员将Java源代码转换为Class文件的首选编译器。对于Java从业人员来说,本书可以让他们学习到Javac是如何支持Java语言的相关特性的,如泛型;而对于对编译器感兴趣的人来说,本书则可以让他们真正学习到如何将编译器的相关理论知识应用到开发实践中。本书内容

本书深入剖析了Javac的源代码实现,每一章都会对重点的源代码实现进行解读。各个章节的内容简单介绍如下:

第1章介绍了Javac的目录结构及源代码调试,同时对Javac支持的编译命令的实现进行了简单介绍。

第2章介绍了Javac操作文件(如读取.java文件、读取JAR包)相关的类。

第3章介绍了Javac将读取到的Java源代码的字符流转换为Token流的过程。

第4章介绍了抽象语法树的每个树节点,这些树节点可以大概划分为定义及声明、语句与表达式。

第5章介绍了如何建立抽象语法树,即Javac根据Token流建立一个完整的抽象语法树。

第6章介绍了符号表的结构,同时也对Javac中使用的符号及类型做了详细介绍。

第7章进行符号表的填充,分两个阶段对抽象语法树进行扫描,然后向符号表中填充相关的符号。

第8章介绍了插入式注解的具体实现过程。

第9章介绍了Java的类型转换,重点介绍了赋值转换、方法调用转换、强制类型转换及数字提升这些上下文环境中的类型转换实现。

第10章介绍了语法检查,主要是对类型定义、变量定义及方法定义的合法性进行检查。

第11章介绍了引用消解,主要是查找表达式中所引用的唯一符号,Resolve类中提供了对类型引用、变量引用及方法引用的具体查找方法。

第12章对抽象语法树进行了类型与符号标注,重点介绍了一些重要树节点,如JCIdent和JCFieldAccess等的具体标注过程。

第13章介绍了泛型的实现。泛型类型或泛型方法等与泛型相关的特性完全由编译器来支持,而在后续生成Class文件的过程中需要对泛型进行擦除。

第14章介绍了数据流分析,分别从变量赋值、语句活跃性及异常这3个方面对if判断语句、循环语句及try语句等进行分析。

第15章介绍了语法糖去除,分别对简单的语法糖、语句语法糖、枚举类语法糖与内部类语法糖进行分析。

第16章介绍了字节码指令的生成,通过简单模拟Java虚拟机运行时的情况来更好地生成字节码指令。

第17章介绍了重要结构的字节码指令生成,对一些常见的控制结构,如if判断语句、循环语句、switch语句及try语句等指令的生成过程进行了详细介绍。

第18章介绍了Class文件的生成,根据Class文件规范生成可被Java虚拟机加载运行的文件。

附录A介绍了Javac支持的命令。

附录B介绍了Java语言涉及的文法。

附录C介绍了对类型的常见操作。

附录D介绍了对符号的常见操作。

附录E介绍了虚拟机字节码指令。本书读者对象

阅读本书需要读者有一定的编程经验,最好是对Java语言有一定的了解。具体而言,本书主要适合以下读者阅读:

·想深入学习Java语言特性的Java从业人员;

·想通过实践学习编译器理论的人员,如高校相关专业的学生;

·想借鉴编译器架构的人员;

·对大型工程源代码感兴趣的人员。本书阅读建议

本书每一章都和前后章形成了承前启后的关系,所以建议读者在阅读本书的过程中,严格按照章节的顺序进行阅读,同时也建议读者在阅读每一章的过程中对书中给出的实例进行实践,以更好地理解本书所讲的内容。

Javac有10万行以上的源代码实现,并且代码的逻辑密度非常大。读者阅读相关源代码的实现时,建议不要过分纠结于每个实现细节,否则会陷入细节的“汪洋大海”中。本书对Javac的重点源代码进行了解读,读者可以参考书中对这些重点源代码的讲解进行阅读、调试即可。本书配套资源获取方式

本书涉及的Javac源代码已经开源,读者可以通过多种途径获取。读者可以直接访问以下网址获取:

https://download.java.net/openjdk/jdk7。读者反馈

由于笔者水平所限,书中可能还会存在一些疏漏,敬请读者指正,笔者会及时进行调整和修改。联系电子邮箱:hzbook2017@163.com。致谢

在本书的写作过程中,得到了很多朋友及同事的帮助和支持,在此表示由衷的感谢!

感谢欧振旭编辑在本书出版过程中给予笔者的大力支持与帮助!

最后感谢我的家人在写作时给予我的理解与支持,在我遇到挫折和问题时,家人都坚定地支持着我。爱你们!马智第1章 Javac介绍

编译器可以将编程语言的代码转换为其他形式,例如,本书讲解的Javac,将Java源代码转换为虚拟机能够识别的Class文件形式(以.class作为文件存储格式),能够进行这种转换的编译器一般也称为编译器的前端。要将Class文件中存储的字节码变为机器码,还需要后端编译器,如JIT编译器(Just In Time Compiler),或者还可以通过AOT编译器直接将Java源代码编译为本地机器代码。本书仅对Java语言的前端编译器Javac进行源代码解读。

之所以对Javac进行源代码解读,是因为Javac支持着Java语言的语法实现,例如泛型的实现完全是由编译器来支持的。另外,通过Javac也能学习编译器相关的知识,它没有使用像Lex、YACC这样的生成器工具,词法、语法分析与代码生成等全都是手工实现的,具有简单、灵活和高效的特点。

在解读Javac代码的过程中会涉及许多的规范,参考的主要规范有:

·Java语法规范(The Java Language Specification,JLS),本书所参照的版本为JLS 7,因此书中再提到JLS时都是指JLS 7。

·Java虚拟机规范(The Java Virtual Machine Specification,JVMS),本书所参照的版本为JVMS 7,因此书中再提到JVMS时都是指JVMS 7。

·Javac在处理注解的过程中还会遵照JSR-269规范,涉及插入式注解处理API(Pluggable Annotation Processing API)。1.1 初识Javac

Javac将Java源代码转变为字节码的过程中会涉及词法分析、语法分析、语义分析及代码生成等阶段,如图1-1所示。图1-1 Javac编译源代码的过程

下面简单来介绍一下这几个不同的编译阶段。

1.词法分析

词法分析的主要作用就是将Java源代码转换为Token流,举个例子如下:【实例1-1】package chapter1;public class TestJavac { int a = 1; int b = a;}

经过Javac的词法分析阶段后转换为Token流,如图1-2所示。图1-2 源代码转换为Token流

图1-2中的每个小方格表示一个具体的Token,其中,箭头(即->)左边的部分为源代码字符串,右边就是对应的Token名称。从图中可以看到,词法分析过程将Java源代码按照Java关键字、自定义标识符和符号等顺序分解为了可识别的Token流,对于空格与换行符等不会生成对应的Token,它们只是作为划分Token的重要依据。

2.语法分析

将进行词法分析后形成的Token流中的Token组合成遵循Java语法规范的语法节点,形成一颗基本的抽象语法树,如图1-3所示。

图1-3中的方格代表抽象语法树节点,而方法中的名称就是Javac对此抽象语法节点的具体实现类,连接线上的名称表示节点属性;其中的两个JCVariableDecl语法树节点,代表两个变量a与b的声明及初始化;vartype属性表示变量声明的类型,而init属性表示对此变量的初始化。图1-3 Token流转换为抽象语法树

3.语义分析

语义分析过程最为复杂,该过程涉及的细节众多,除了对代码编写者写出的源代码根据JLS规范进行严格的检查外,还必须为后面的代码生成阶段准备各种数据,如符号表、标注抽象语法树节点的符号及类型等。在标注语法树的过程中会确定符号的具体引用及进行类型检查,如将变量a的值赋值给变量b时就会确定a所对应的具体引用,确定后a将指向变量a的定义,根据变量a定义时的类型与b声明的类型做类型的兼容性检查。

4.代码生成

将标注语法树转化成字节码,并将字节码写入Class文件。通过命令javap -verbose TestJavac来查看Class文件的相关内容,代码如下:public class chapter1.TestJavac minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #5.#15 // java/lang/Object."":()V #2 = Fieldref #4.#16 // chapter1/TestJavac.a:I #3 = Fieldref #4.#17 // chapter1/TestJavac.b:I #4 = Class #18 // chapter1/TestJavac #5 = Class #19 // java/lang/Object #6 = Utf8 a #7 = Utf8 I #8 = Utf8 b #9 = Utf8 #10 = Utf8 ()V #11 = Utf8 Code #12 = Utf8 LineNumberTable #13 = Utf8 SourceFile #14 = Utf8 TestJavac.java #15 = NameAndType #9:#10 // "":()V #16 = NameAndType #6:#7 // a:I #17 = NameAndType #8:#7 // b:I #18 = Utf8 chapter1/TestJavac #19 = Utf8 java/lang/Object{ int a; descriptor: I flags: int b; descriptor: I flags: public chapter1.TestJavac(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: aload_0 10: aload_0 11: getfield #2 // Field a:I 14: putfield #3 // Field b:I 17: return LineNumberTable: line 3: 0 line 4: 4 line 5: 9}SourceFile: "TestJavac.java"

将Java代码转化为符合Java虚拟机规范的字节码,然后写入TestJavac.class文件中。如果读者看不懂字节码也没有关系,第16章及第18章将详细介绍字节码指令及Class文件的格式。1.2 Javac源码与调试

首先需要下载openJDK源码,本书涉及的源码版本都是JDK 1.7,读者可以到https://download.java.net/openjdk/jdk7下载源代码的ZIP包,本书下载的源代码包为openjdk-7-fcs-src-b147-27_jun_2011.zip。

解压后可以在openjdk/langtools/src/share/classes/com/sun/tools路径下找到javac目录,该目录下存放着Javac主要的源代码实现,可以将相关的源代码复制到IDE中,这样就可以借助Eclipse等IDE调试源代码了。

首先在Eclipse中新建Java项目,名称为JavacCompiler,然后将openjdk/langtools/src/ share/classes/路径下的com目录复制到项目的src目录下,最后的项目结构如图1-4和图1-5所示。图1-4 JavacCompiler项目结构图1-5 javac包下的目录结构

在图1-4中,com.sun.javac目录下存放着Javac主要的源代码实现,而com.sun.source目录下存放着Javac相关的依赖,我们只需要关注图1-4中用方框圈起来的部分即可。图1-5中给出了javac目录的详细结构。

在Eclipse中导入代码后可能会在类com.sun.tools.javac.model.AnnotationProxyMaker. java上提示Access Restriction错误。由于Eclipse的JRE System Library中默认包含了一系列的代码访问规则,如果代码中引用了这些访问规则所禁止引用的类,就会提示此错误。可以通过添加一条允许访问JAR包中所有类的访问规则来解决此问题。在项目上右击,在弹出的快捷菜单中选择Properties命令,弹出Properties for JavacCompiler对话框。在对话框左侧列表中选择Java Build Path后,切换到右侧的Libraries选项卡,选择Access rules后单击Edit按钮,为Access rules添加一条Resolution为Accessible、值为“**”的访问规则,添加后如图1-6所示。图1-6 为Access rules添加访问规则

如表1-1所示为对com.sun.tools.javac及com.sun.source包下一些重要的目录结构的说明。表1-1 Javac主要目录说明

在本书后续的源代码解读过程中,将详细介绍这些包下相关类的实现。1.3 Javac命令

Javac提供了一些命令,用于支持Java源文件的编译。如果安装且配置了Java的PATH路径,可在Windows的命令行窗口中输入java -help命令查看,或者直接查看Javac源码中的枚举类com.sun.tools.javac.main.OptionName,其中列举了所有当前Javac版本所支持的命令。

如果在命令行窗口中输入Javac命令时,格式如下:javac [options] [sourceFiles] [@argFiles]

options是指命令行选项,sourceFiles是指一个或多个Java源文件,@argFiles是指列出选项和源文件的一个或者多个文件。

可以通过以下方式之一将Java源代码文件名传递给Javac:

·在命令行中列出文件名,如果Java源文件的数量不多,可以使用这种方式;

·在一个文件中列出文件名,两个名称之间用空格或者换行符隔开,然后将列表文件的路径传递给前面带有@符号的Javac命令行。

不过现在可以直接通过编写代码的方式调用Javac,其效果和在命令行窗口中输入Javac命令是一样的。其实Javac命令最终也是通过调用Javac相关的API完成Java源代码编译的。可以在JavacCompiler项目中新建一个test源文件目录,然后在包路径chapter1下新建TestCompiler.java源文件,具体内容如下:【实例1-2】package chapter1;import java.io.IOException;import javax.tools.ToolProvider;public class TestCompiler { public static void main(String args[]) throws IOException { String path = "C:/JavacCompiler/test/chapter1/TestJavac.java"; javax.tools.JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); int result = compiler.run(null, null, null, new String[]{ "-d","C:/javacclass", path } ); System.out.println("Result code: " + result); }}

以上代码在调用compiler.run()方法时传递了一个字符串数组作为参数,在这个数组中就可以指定命令行选项及要编译的源文件名,运行main()方法等价于运行如下命令:javac -d C:/javacclass C:/JavaCompiler/test/chapter1/TestJavac.java

其中,-d表示生成的class文件的存放路径,这里是C:/javacclass。运行main()方法后打印如下信息:Result code:0

调用compiler.run()方法若返回0则表示运行成功,若返回非0则表示有编译错误。

Javac本身支持的命令很多,通常编译器对外提供的命令可以分为标准和非标准两个类型,非标准选项以-X开头。但是Javac内部却将命令分为3类,并通过枚举类OptionKind来表示,示例如下:来源:com.sun.tools.javac.main.JavacOption.OptionKindenum OptionKind { NORMAL, EXTENDED, HIDDEN,}

枚举常量NORMAL就是通常所说的标准选项,而EXTENDED就是非标准选项,或者说扩展选项。另外还有一类HIDDEN,这是Javac内部使用的选项。例如,OptionName中定义的-warnunchecked命令,表示对泛型的非检查异常给出警告,-fullversion命令打印完整的版本信息。关于Javac支持的标准和非标准选项,可以参考附录A,这里不做过多介绍。

下面看一下这些命令在Javac中的实现。每个命令都是JavacOption对象,这个接口中定义了对命令的常见操作,源代码实现如下:来源:com.sun.tools.javac.main.JavacOptionpublic interface JavacOption { OptionKind getKind(); boolean hasArg(); boolean matches(String arg); boolean process(Options options, String option, String arg); boolean process(Options options, String option); }

其中,getKind()方法会返回OptionKind中定义的枚举常量,表示当前这个命令属于哪一类;hasArgs()方法返回true时表示这个命令有相关参数,例如,-d命令后面指定的路径就是这个命令的一个参数;matches()方法判断传入的命令是否与当前的命令匹配;两个process()方法就是对命令的具体执行了,只是一个执行有参数的命令,另外一个执行无参数的命令。

JavacOption接口主要有3个实现类,继承体系如图1-7所示。图1-7 JavacOption继承体系

Option代表了标准选项,因此调用getKind()方法将返回NORMAL;XOpion代表非标准选项,调用getKind()方法返回EXTENDED;HiddenOption代表内部选项,调用getKind()方法返回HIDDEEN。所有命令可通过这3个实现类或通过匿名类继承这几个类来实现,然后根据具体的命令选择性地实现相关的方法,具体实现可查看com.sun.tools.javac.main. RecognizedOptions类中的getAll()方法。对于实例1-2来说,-d与sourcefile命令的实现很典型,代码如下:来源:com.sun.tools.javac.main.RecognizedOptionspublic static Option[] getAll(final OptionHelper helper) { return new Option[]{ ... new Option(D,"opt.arg.directory","opt.d"), ... new HiddenOption(SOURCEFILE) { // sourcefile命令的匿名类实现 String s; @Override public boolean matches(String s) { this.s = s; return s.endsWith(".java") // Java source file || SourceVersion.isName(s); // Legal type name } @Override public boolean process(Options options, String option) { if (s.endsWith(".java") ) { File f = new File(s); helper.addFile(f); } return false; } }, };}

在查看所有的命令实现时,我们要关注的重点就是process()方法的实现。对于-d命令则创建了一个Option对象(Option类型的对象,简称Option对象,后续都使用这样的描述方式),其中,process()方法的默认实现会将命令存储到Options对象的options中。Options类中定义了一个类型为LinkedHashMap的values变量,其中key为具体的命令,而value就是对应的参数。例如,在实例1-2中将存储key为-d而value为C:/javacclass的一条数据。

用一个HiddenOption匿名类对sourcefile命令进行实现,process()方法中会调用helper.addFile()方法,helper的类型为OptionHelper,从OptionHelper类的名字也不难看出,它是用来辅助进行命令实现的。由于一些复杂的命令执行可能会用到许多的类对象,如工具类的对象,而定义在Main类中的OptionHelper对象能使用更多类提供的API来实现命令。每个命令都是一个具体的JavacOption对象,这些对象最终将存储到Option数组中返回给getAll()方法的调用者。

OptionHelper是个接口,这个接口有个匿名类的实现,是在com.sun.tools.javac.main. Main类中定义recognizedOptions变量时实现的,代码如下:来源:com.sun.tools.javac.main.Mainprivate Option[] recognizedOptions = RecognizedOptions.getJavaCompilerOptions(new OptionHelper() { ... public void addFile(File f) { if (!filenames.contains(f)) filenames.append(f); } ...});

匿名类对OptionHelper接口中定义的方法进行了实现。例如,在实现sourcefile命令时调用了helper的addFile()方法,该方法最终会将具体的文件对象追加到类型为ListBuffer 的对象filenames中,供后续编译阶段使用。

OptionHelper匿名类对象最终会作为参数传递给RecoginizedOptions类的getJava CompilerOptions()方法,调用getJavaCompilerOptions()方法其实也是间接调用getAll()方法。最终recognizedOptions就以数组的形式保存了Option对象,这样当用户传递相关的编译命令时就可以循环这个数组找到对应命令的Option对象,然后调用相关的方法对命令进行操作了。

对于实例1-2来说,调用compiler.run()方法最终会调用Main类中的compile()方法,该方法有如下代码调用:来源:com.sun.tools.javac.main.MainList files = processArgs(CommandLine.parse(args));

其中的args就是编译文件时传递的参数,也就是实例1-2中传递的字符串数组。调用CommandLine.parse()方法主要是解决以@argFiles方式指定编译文件的形式,这些路径下的文件内容最终会读取出来追加到数组的末尾。

下面来看processArgs()方法的具体实现,代码如下:来源:com.sun.tools.javac.main.Mainpublic List processArgs(String[] flags) { int ac = 0; while (ac < flags.length) { String flag = flags[ac]; ac++; Option option = null; if (flag.length() > 0) { int firstOptionToCheck = flag.charAt(0) == '-' ? 0 : recognizedOptions.length-1; for (int j=firstOptionToCheck; j

方法循环处理传入的所有参数,如果是具体的命令,就找到命令所对应的具体的option。由于命令一般都是以“-”开头的,因而如果以“-”开头就循环recognizedOptions数组查找,找到对应的option后调用process()方法执行具体的命令;如果不以“-”开头,例如,实例1-2中path传递的是具体要编译的Java源代码路径,则直接查找recongnized Options数组中的最后一项,即sourcefile命令。执行sourcefile后会调用helper的addFile()方法,而helper的addFile()方法会将需要编译的Java源文件路径追加到filenames,这样processArgs()方法执行完成后以列表形式返回filenames,Javac会对filenames列表中指定的源文件进行编译。

如果还传递其他一些命令,如-version,可以循环找到-version在recognizedOptions中的匿名类对象并调用process()方法,最终将通过调用helper的printVersion()方法打印当前JDK的版本号。对于大部分的命令,需要在后续编译过程的各个阶段使用,因此暂时存储到options中。第2章 Javac文件系统

Javac在编译源代码的过程中会涉及对文件及目录的操作,例如,在指定路径下搜索Java源文件,读取JAR包中的Class文件,以及将编译生成的字节码写入Class文件等。Javac文件系统借助Java已有的文件API实现了满足自身业务需求相关的API,方便了对文件及目录的操作。Javac文件系统涉及的大部分类都存放在com.sun.tools.javac.file包路径下,如果有不在此路径下的类出现,笔者在首次提到该类时,会给出类的全限定名。2.1 文件相关实现类

首先考虑一下Javac在编译源代码过程中需要操作哪些类型的文件。由于是编译Java源代码,因而肯定会读取.java结尾的文件;在编译.java结尾的文件时少不了读取依赖,这些依赖大多都是保存在.class结尾的文件中,因此也需要操作.class文件;由于.class结尾的文件可能在压缩包中,例如常见的JAR包,因此还需要读取压缩包的内容;如果要使用JDK中rt.jar包提供的一些类库API,那么还会读取JAVA_HOME路径下的ct.sym文件,这个文件也是一个压缩包。之所以不直接读取rt.jar包,是为了避免开发人员使用一些内部的API,同时也能避免代码编写者调整这些接口时造成客户端代码无法运行。

Javac中处理最多的就是.class与.java结尾的文件,这些文件在Javac中都以特定类的对象表示并且需要专门进行管理。首先来看压缩包涉及的主要类的继承体系,如图2-1所示。图2-1 Javac中压缩包类的继承体系

图2-1表示的是压缩包相关类的继承体系,有两个类直接继承了Archive接口,该接口定义在JavacFileManager类中。JavacFileManager类是Javac的整个文件系统管理类,当然也管理着实现了Archive接口的所有压缩包类。通过ZipArchive类可以操作除rt.jar包外的所有JAR包,包括ct.sym包,SymbolArchive类操作ct.sym包。通常,在读取非ct.sym包时会选择ZipArchive或者ZipFileIndexArchive类,读取ct.sym包时选择SymbolArchive或者ZipFileIndexArchive类。在JDK 1.7版本下的Javac默认都是通过ZipFileIndexArchive类来读取压缩包的。可以在Javac中指定useOptimizedZip命令的值为false,来使用ZipArchive或者SymbolArchive类来读取压缩包,这两个类在读取压缩包的过程中都使用了JDK本身提供的API,实现起来相对简单,读取时也不用关注压缩包相关的格式等,而ZipFileIndexArchive类的读取效率相比ZipArchive和SymbolArchive这两个类更高,但是实现相对复杂。本书将详细介绍默认的ZipFileIndex类的实现方式。

Javac内部的每个压缩包都是一个ZipFileIndex对象,而对应每个Archive接口的实现类中都定义了表示压缩包中具体压缩文件的静态内部类,这些类的继承体系如图2-2所示。图2-2 文件相关类的继承体系

其中,ZipFileObject类定义在ZipArchive类中;SymbolFileObject类定义在SymbolArchive类中;ZipFileIndexFileObject类定义在ZipFileIndexArchive类中。它们都实现了BaseFileObject抽象类。这个抽象类中定义了针对具体文件进行操作的许多方法,例如调用getCharContent()方法,可以获取对应文件的字符流。另外还有一个实现类RegularFileObject,Java源文件一般用这个类来表示。

下面来看ZipFileIndexArchive类的实现。

ZipFileIndexArchive类是默认操作所有压缩包的类,不过Javac主要用这个类来读取ct.sym及JAR包中的内容。ZipFileIndexArchive类的实现如下:来源:com.sun.tools.javac.file.ZipFileIndexArchivepublic class ZipFileIndexArchive implements Archive { private final ZipFileIndex zfIndex; private JavacFileManager fileManager; // 通过相对路径subdirectory查找所有的文件,以列表的形式返回所有文件的名称 public List getFiles(RelativeDirectory subdirectory) { return zfIndex.getFiles(subdirectory); } // 通过相对路径subdirectory查找名称为file的文件 public JavaFileObject getFileObject(RelativeDirectory subdirectory,String file) { RelativeFile fullZipFileName = new RelativeFile(subdirectory, file); Entry entry = zfIndex.getZipIndexEntry(fullZipFileName); JavaFileObject ret = new ZipFileIndexFileObject(fileManager,zfIndex,entry,zfIndex.getZipFile()); return ret; }}

这个类实现了Archive接口并且定义了两个成员变量,其中zfIndex变量保存具体的压缩包,每个压缩包都是一个ZipFileIndex对象。另外还定义了两个读取压缩包内容的方法getFiles()与getFileObject(),getFiles()方法可以读取匹配相对路径subdirectory下的所有文件,而getFileObject()方法可以根据相对路径及文件名获取到一个具体的文件,其实就是一个ZipFileIndexFileObject对象,这样后续就可以调用JavaFileObject类中提供的相关方法进行文件操作了。从实现来看,两个方法都会调用ZipFileIndex类中的相关方法进行实现。ZipFileIndex类的定义如下:来源:com.sun.tools.javac.file.ZipFileIndexpublic class ZipFileIndex { final File zipFile; ZipFileIndex(File zipFile, _, _, _, _) throws IOException { this.zipFile = zipFile; checkIndex(); } // 通过相对路径path查找所有的文件,以列表的形式返回所有文件的名称 public synchronized com.sun.tools.javac.util.List getFiles(RelativeDirectory path) { checkIndex(); DirectoryEntry de = directories.get(path); com.sun.tools.javac.util.List ret = null; if(de!=null){ ret = de.getFiles(); } if (ret == null) { return com.sun.tools.javac.util.List.nil(); } return ret; } // 通过相对路径path查找文件 public synchronized Entry getZipIndexEntry(RelativePath path) { checkIndex(); DirectoryEntry de = directories.get(path.dirname()); String lookFor = path.basename(); return (de == null) ? null : de.getEntry(lookFor); }}

zipFile保存要读取的压缩包,getFiles()与getZipIndexEntry()方法是ZipFileIndexArchive类中实现getFiles()与getFileObject()方法时调用的方法。无论是在ZipFileIndex类的构造方法还是这两个方法中,首先都会调用checkIndex()方法读取压缩包相关的内容。例如,读取所有的目录并存储到directories列表中,这样就可以根据目录的相对路径获取到DirectoryEntry对象了,然后在getZipIndexEntry()方法中调用此对象的getEntry()方法获取一个具体表示目录或文件的Entry对象,或者在getFiles()方法中调用对象的getFiles()方法来获取相对路径下包含的所有文件的名称。

首先来看checkIndex()方法对压缩包的读取过程,然后再看DirectoryEntry类中提供的getFiles()与getEntry()方法的实现。checkIndex()方法的实现如下:来源:com.sun.tools.javac.file.ZipFileIndexprivate void checkIndex() throws IOException { openFile(); // 初始化zipRandomFile变量 long totalLength = zipRandomFile.length(); ZipDirectory directory = new ZipDirectory(zipRandomFile, 0L,totalLength, this); directory.buildIndex(); // 为压缩包建立读取索引}

checkIndex()方法首先调用openFile()方法初始化zipRandomFile变量,然后将zipRandomFile封装为ZipDirectory对象并调用buildIndex()方法建立读取索引,这样就可以高效读取压缩包相关的内容了。

zipRandomFile是在ZipFileIndex类中定义的一个RandomAccessFile类型的成员变量,用来保存具体要读取的压缩包。初始化过程如下:来源:com.sun.tools.javac.file.ZipFileIndexprivate void openFile() throws FileNotFoundException { if (zipRandomFile == null && zipFile != null) { zipRandomFile = new RandomAccessFile(zipFile, "r"); }}

zipFile已经在ZipFileIndex类的构造方法中初始化过了,因此当zipRandomFile为空并且zipFile不为空时创建一个RandomAccessFile对象并赋值给成员变量zipRandomFile。

ZipDirectory类是ZipFileIndex类内定义的一个私有成员类,这个类中的相关方法将按照压缩包的格式从zipRandomFile中读取压缩包中的目录和文件,然后保存到ZipFileIndex类中一个全局私有的变量entries中,供其他方法查询。ZipDirectory类的实现如下:来源:com.sun.tools.javac.file.ZipFileIndex.ZipDirectoryprivate class ZipDirectory { byte[] zipDir; RandomAccessFile zipRandomFile = null; ZipFileIndex zipFileIndex = null; public ZipDirectory(RandomAccessFile zipRandomFile, long start, longend, ZipFileIndex index) throws IOException { this.zipRandomFile = zipRandomFile; this.zipFileIndex = index; findCENRecord(start, end); } private void findCENRecord(long start, long end) throws IOException { long totalLength = end - start; int endbuflen = 1024; byte[] endbuf = new byte[endbuflen]; long endbufend = end - start; while (endbufend >= 22) { if (endbufend < endbuflen) endbuflen = (int)endbufend; long endbufpos = endbufend - endbuflen; zipRandomFile.seek(start + endbufpos); zipRandomFile.readFully(endbuf, 0, endbuflen); int i = endbuflen - 22; // 让i指向End of central directory record中Signature(签名)的 第一个字节位置 while (i >= 0 && !(endbuf[i] == 0x50 && endbuf[i + 1] == 0x4b && endbuf[i + 2] == 0x05 && endbuf[i + 3] == 0x06 && endbufpos + i + 22 + get2ByteLittleEndian(endbuf, i + 20)== totalLength)) { i--; } // 此时的i已经指向End of central directory record中Signature(签 名)的第一个字节位置 if (i >= 0) { zipDir = new byte[get4ByteLittleEndian(endbuf, i + 12) + 2]; // 读取File header数量 zipDir[0] = endbuf[i + 10]; zipDir[1] = endbuf[i + 11]; int sz = get4ByteLittleEndian(endbuf, i + 16); zipRandomFile.seek(start + sz); // 读取所有File header的内容并保存到zipDir数组中 zipRandomFile.readFully(zipDir, 2, zipDir.length - 2); return; } else { endbufend = endbufpos + 21; } } throw new ZipException("cannot read zip file"); }}

在构造方法中初始化zipRandomFile与zipFileIndex变量,调用findCENRecord()方法初始化zipDir变量。在初始化zipDir的过程中会涉及对压缩包内容的读取,因此需要简单介绍一下压缩包的基本格式,如图2-3所示。

我们只关注Central directory(中央目录)的内容,它处在文件的末尾,具体的格式如图2-4所示。图2-3 压缩包的基本格式图2-4 Central directory的格式

现在我们所关注的内容是每一个File header(文件头),首先读取File header的数量并保存到zipDir字节数组最开始的两个字节也就是数组下标为0和1的位置,然后将第一个到最后一个File header的字节内容存储到zipDir字节数组中从下标为2开始的位置,这样就相当于为要读取的压缩包内容建立了索引。相关信息可通过End of central directory record(中央目录区结尾记录)来读取,具体格式如图2-5所示。图2-5 End of central directory record的格式

其中,Total entries记录了在Central directory中压缩条目的总数,而Central directory size表示中央目录区的字节数大小,可通过调用get4ByteLittleEndian()方法读取这个值。在ZipDirectory类的findCENRecord()方法中可以看到,zipDir数组的大小被初始化为如下表达式的值:get4ByteLittleEndian(endbuf, i + 12) + 2

其中,调用get4ByteLittleEndian()方法就是为了得到Central directory的字节数大小,由于数组头两个字节要保存File header的数量,因而要加2。

findCENRecord()方法通过循环让i指向End of central directory record中Signature(签名)的第一个字节位置,其中的Signature是一个固定的值“\x50\x4b\x05\x06”。

初始化了zipDir后就可以在ZipFileIndex类的checkIndex()方法中调用buildIndex()方法读取具体压缩包中的内容了。buildIndex()方法的实现如下:来源:com.sun.tools.javac.file.ZipFileIndex.ZipDirectoryprivate class ZipDirectory { byte[] zipDir; RandomAccessFile zipRandomFile = null; ZipFileIndex zipFileIndex = null; public ZipDirectory(RandomAccessFile zipRandomFile, long start, longend, ZipFileIndex index) throws IOException { this.zipRandomFile = zipRandomFile; this.zipFileIndex = index; findCENRecord(start, end); } private void findCENRecord(long start, long end) throws IOException { long totalLength = end - start; int endbuflen = 1024; byte[] endbuf = new byte[endbuflen]; long endbufend = end - start; while (endbufend >= 22) { if (endbufend < endbuflen) endbuflen = (int)endbufend; long endbufpos = endbufend - endbuflen; zipRandomFile.seek(start + endbufpos); zipRandomFile.readFully(endbuf, 0, endbuflen); int i = endbuflen - 22; // 让i指向End of central directory record中Signature(签名)的 第一个字节位置 while (i >= 0 && !(endbuf[i] == 0x50 && endbuf[i + 1] == 0x4b && endbuf[i + 2] == 0x05 && endbuf[i + 3] == 0x06 && endbufpos + i + 22 + get2ByteLittleEndian(endbuf, i + 20)== totalLength)) { i--; } // 此时的i已经指向End of central directory record中Signature(签 名)的第一个字节位置 if (i >= 0) { zipDir = new byte[get4ByteLittleEndian(endbuf, i + 12) + 2]; // 读取File header数量 zipDir[0] = endbuf[i + 10]; zipDir[1] = endbuf[i + 11]; int sz = get4ByteLittleEndian(endbuf, i + 16); zipRandomFile.seek(start + sz); // 读取所有File header的内容并保存到zipDir数组中 zipRandomFile.readFully(zipDir, 2, zipDir.length - 2); return; } else { endbufend = endbufpos + 21; } } throw new ZipException("cannot read zip file"); }}

以上代码中,调用get2ByteLittleEndian()方法读取zipDir数组中前两个字节中保存的File header数量,调用readEntry()方法从zipDir中读取每一个具体的File header并将读取到的内容填充到entryList与directories集合中。directories变量的定义如下:来源:com.sun.tools.javac.file.ZipFileIndexprivate Map directories = Collections.emptyMap();

directories中保存了压缩包中相对路径到DirectoryEntry对象的映射关系,DirectoryEntry类是ZipFileIndex类中定义的一个静态内部类,表示具体的目录。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载