Java 9模块化开发:核心原则与实践(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-29 16:49:07

点击下载

作者:(荷)桑德·马克(Sander Mak),王净

出版社:机械工业出版社

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

Java 9模块化开发:核心原则与实践

Java 9模块化开发:核心原则与实践试读:

前言

Java 9向Java平台引入了模块系统,这是一个重大的飞跃,标志着Java平台上模块化软件开发的一个新时代的开始。看到这些变化让人感到非常兴奋,希望读者看完本书后也会感到兴奋。在深入了解模块系统之前需要做好充分利用该系统的准备。

本书读者

本书为那些想要提高应用程序的设计和结构的Java开发者而编写。Java模块系统改进了设计和构建Java应用程序的方法。即使你不打算马上使用模块,了解JDK模块化本身也是非常重要的一步。在熟悉了本书第一部分所介绍的模块之后,希望你也能真正理解后续关于迁移的相关章节。

将现有代码移至Java 9和模块系统将成为一项越来越常见的任务。

本书绝不是对Java的一般性介绍。我们假设你拥有在一个团队中编写过较大Java应用程序的经验,在较大的Java应用程序中模块变得越来越重要。作为一名经验丰富的Java开发人员,应该认识到类路径所带来的问题,从而有助于理解模块系统及其功能。

除了模块系统之外,Java 9中还有许多其他变化。然而,本书主要关注模块系统及其相关功能。当然,在适当的情况下,在模块系统的上下文中也会讨论其他Java 9功能。

编写本书的原因

很多读者从Java早期开始就是Java用户,当时Applet还非常流行。多年来,我们使用和喜欢过许多其他平台和语言,但Java仍然是主要工具。在构建可维护的软件方面,模块化是一个关键原则。多年来人们花费了大量精力来构建模块化软件,并逐渐热衷于开发模块化应用程序。曾经广泛使用诸如OSGi之类的技术来实现模块化,但Java平台本身并不支持这些技术。此外,还可通过Java之外的其他工具学习模块化,比如JavaScript的模块系统。当Java 9推出了期待已久的模块系统时,我们认为并不能只是使用该功能,还应该帮助其刚入职的开发人员了解模块系统。

也许在过去10年的某个时候你曾经听说过Jigsaw项目。经过多年的发展,Jigsaw项目具备了Java模块系统许多功能的原型。Java的模块系统发展断断续续。Java 7和Java 8最初计划包含Jigsaw项目的发展结果。

随着Java 9的出现,长期的模块化尝试最终完成了正式模块系统的实现。多年来,各种模块系统原型的范围和功能发生了许多变化。即使你一直在密切关注该过程,也很难弄清楚最终Java 9模块系统真正包含什么。本书将会给出模块系统的明确概述,更重要的是将介绍模块系统能够为应用程序的设计和架构做些什么。

本书内容

本书共分为三个部分:

1)Java模块系统介绍。

2)迁移。

3)模块化开发工具。

第一部分主要介绍如何使用模块系统。首先从介绍模块化JDK本身开始,然后学习创建自己的模块,随后讨论可以解耦模块的服务,最后探讨模块化模式以及如何以最大限度地提高可维护性和可扩展性的方式使用模块。

第二部分主要介绍迁移。有可能读者现在所拥有的Java代码不是使用专为模块系统而设计的Java库。该部分介绍如何将现有代码迁移到模块中,以及如何使用尚未模块化的现有库。如果你是一名库的编写者或者维护者,那么这部分中有一章专门介绍了如何向库添加模块支持。

第三部分(也是最后一部分)主要介绍工具。该部分介绍了IDE的现状以及构建工具。此外还会学习如何测试模块,因为模块给(单元)测试带来了一些新的挑战,也带来了机会。最后学习链接(linking)——模块系统另一个引人注目的功能。

虽然建议从头到尾按顺序阅读本书,但是请记住并不是所有的读者都必须这样阅读。建议至少详细阅读前四章,从而具备基本知识,以便更好地阅读本书的其他章节。如果时间有限并且有现有的代码需要迁移,那么可以在阅读完前四章后跳到本书的第二部分。一旦完成了迁移,就可以回到“更高级”的章节。

使用代码示例

本书包含了许多代码示例。所有代码示例都可以在GitHub(https://github.com/java9-modularity/examples)上找到。在该存储库中,代码示例是按照章节组织的。在本书中,使用下面的方法引用具体的代码示例:chapter3/helloworld,其含义是可以在“https://github.com/java9-modularity/examples/chapter3/helloworld”中找到示例。

强烈建议在阅读本书时使用相关的代码,因为在代码编辑器中可以更好地阅读较长的代码段。此外还建议亲自动手改写代码,如重现书中所讨论的错误。动手实践胜过读书。

排版约定

下面列出的是书中所使用的字体约定:

斜体(Italic)

表示新术语、URL、电子邮件地址、文件名以及文件扩展名。

等宽字体(Constant width)

用于程序清单,以及在段落中引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。

等宽粗体(Constant width bold)

显示应由用户逐字输入的命令或其他文本。

等宽斜体(Constant width italic)

显示应该由用户提供的值或由上下文确定的值所替换的文本。该图标表示一般注释。该图标表示提示或者建议。该图标表示警告或者提醒。

Safari在线电子书

Safari(前身为Safari Books Online)是一个基于会员制的为企业、政府、教育工作者和个人提供培训和参考的平台。

会员可以访问来自250家出版商的书籍、培训视频、学习路径、交互式教程和精心策划的播放列表,包括O'Reilly Media、Harvard Business Review、Prentice Hall Professional、Addison-Wesley Professional、Microsoft Press、Sams、Que、Peachpit Press、Adobe、Focal Press、Cisco Press、John Wiley&Sons、Syngress、Morgan Kaufmann、IBM Redbooks、Packt、AdobePress、FT Press、Apress、Manning、New Riders、McGraw-Hill、Jones&Bartlett,以及Course Technology,等等。

更多信息,请访问http://oreilly.com/safari。

如何联系我们

对于本书,如果有任何意见或疑问,请按照以下地址联系本书出版商。

美国:

O'Reilly Media,Inc.

1005Gravenstein Highway North

Sebastopol,CA 95472

中国:

北京市西城区西直门南大街2号成铭大厦C座807室(100035)

奥莱利技术咨询(北京)有限公司

要询问技术问题或对本书提出建议,请发送电子邮件至:

bookquestions@oreilly.com

要获得更多关于我们的书籍、会议、资源中心和O’Reilly网络的信息,请参见我们的网站:

http://www.oreilly.com

http://www.oreilly.com.cn

我们在Facebook上的主页:http://facebook.com/oreilly

我们在Twitter上的主页:http://twitter.com/oreillymedia

我们在YouTube上的主页:http://www.youtube.com/oreillymedia

致谢

编写本书的想法来源于2015年在JavaOne会议上与来自O’Reilly的Brian Foster的一次谈话,非常感谢你委托我们参与这个项目。从那时起,很多人对本书的编写提供了帮助。

感谢Alex Buckley、Alan Bateman和Simon Maple所给出的重要技术评论和对本书所提出的许多改进意见。此外,还要感谢O’Reilly的编辑团队,Nan Barber和Heather Scherer考虑到了所有的组织细节。

如果没有妻子Suzanne的坚定支持,编写本书是不可能的。多少个夜晚和周末,我都无法陪伴妻子和三个孩子。感谢你一直陪我到最后!此外,还要感谢Luminis(http://luminis.eu/)为编写本书所提供的支持。我很高兴能成为公司的一员,我们的口号是“知识是共享的唯一财富”。Sander Mak

我也要感谢妻子Qiushi,在我编写这第二本书籍时始终支持我,即使在我们搬到世界的另一个位置的时候。此外,还要感谢Netflix(http://netflix.com/)和Luminis(http://luminis.eu/),感谢它们给予我编写本书的时间和机会。Paul Bakker

本书第1章、第7章、第13章和第14章的漫画由Oliver Widder(http://geek-and-poke.com/)创建,并获得Creative Commons Attribution 3.0Unported(CC BY 3.0)(http://creativecommons.org/licenses/by/3.0/deed.en_US)的许可。本书的作者将漫画改为横向和灰色。第一部分Java模块系统介绍第1章模块化概述

你是否曾经因为困惑而不停地挠头,并问自己:“代码为什么在这个位置?它们如何与其他庞大的代码块相关联?我该从哪里开始呢?”或者在看完应用程序代码所捆绑的大量Java归档文件(Java Archive,JAR)后,目光是否会呆滞?答案是肯定的。

构建大型代码库是一项被低估的技术。这既不是一个新问题,也不是Java所特有的。然而,Java一直是构建大型应用程序的主流语言之一,且通常会大量使用Java生态系统中的许多库。在这种情况下,系统可能会超出我们理解和有效开发的能力范围。经验表明,从长期来看,缺乏结构性所付出的代价是非常高的。

模块化是用来管理和减少这种复杂性的技术之一。Java 9引入了一个新的模块系统,从而可以更容易地创建和访问模块。该系统建立在Java已用于模块化开发的抽象基础之上。从某种意义上讲,它促使了现有的大型Java开发最佳实践成为Java语言的一部分。

Java模块系统对Java开发产生了深刻的影响。它代表了模块化成为整个Java平台高级功能这一根本转变,从根本上解决了模块化的问题,对语言、JVM(Java Virtual Machine,Java虚拟机)以及标准库都进行了更改。虽然完成这些更改付出了巨大的努力,但却并不像Java 8中添加流和Lambda表达式那样“华而不实”。诸如Lambda表达式之类的功能与Java模块系统之间还存在另一个根本区别。模块系统关注的是整个应用程序的大型结构,而将内部类转换为一个Lambda表达式则是单个类中相当小的局部变化。对一个应用程序进行模块化会影响设计、编译、打包、部署等过程,显然,这不仅仅是另一种语言功能。

随着新Java版本的发布,你可能会迫不及待地想使用这个新功能了。为了充分利用模块系统,首先后退一步,先了解一下什么是模块化,更重要的是为什么要关注模块化。1.1 什么是模块化

到目前为止,所讨论的只是实现模块化的目标(管理和减少复杂性),却没有介绍实现模块化需要什么?从本质上讲,模块化(modularization)是指将系统分解成独立且相互连接的模块的行为。模块(module)是包含代码的可识别工件,使用了元数据来描述模块及其与其他模块的关系。在理想情况下,这些工件从编译时到运行时都是可识别的。一个应用程序由多个模块协作组成。

因此,模块对代码进行了分组,但不仅于此。模块必须遵循以下三个核心原则:1.强封装性

一个模块必须能够对其他模块隐藏其部分代码。这样一来,就可以在可公开使用的代码和被视为内部实现细节的代码之间划定一条清晰的界限,从而防止模块之间发生意外或不必要的耦合,即无法使用被封装的内容。因此,可以在不影响模块用户的情况下自由地对封装代码进行更改。2.定义良好的接口

虽然封装是很好的做法,但如果模块需要一起工作,那么就不能将所有的内容都进行封装。从定义上讲,没有封装的代码是模块公共API的一部分。由于其他模块可以使用这些公共代码,因此必须非常小心地管理它们。未封装代码中任何一个重大更改都可能会破坏依赖该代码的其他模块。因此,模块应该向其他模块公开定义良好且稳定的接口。3.显式依赖

一个模块通常需要使用其他模块来完成自己的工作,这些依赖关系必须是模块定义的一部分,以便使模块能够独立运行。显式依赖会产生一个模块图:节点表示模块,而边缘表示模块之间的依赖关系。拥有模块图对于了解应用程序以及运行所有必要的模块是非常重要的。它为模块的可靠配置提供了基础。

模块具有灵活性、可理解性和可重用性。模块可以灵活地组合成不同的配置,利用显式依赖来确保一切工作正常。封装确保不必知道实现细节,也不会造成无意识间对这些细节的依赖。如果想要使用一个模块,只需知道其公共API就可以了。此外,如果模块在封装了实现细节的同时公开了定义良好的接口,那么就可以很容易地使用符合相同API的实现过程来替换被封装的实现过程。

模块化应用程序拥有诸多优点。经验丰富的开发人员都知道使用非模块化的代码库会发生什么事情。诸如意大利面架构(spaghetti architecture)、凌乱的巨石(messy monolith)以及大泥球(big ball of mud)之类的术语都描述了由此所带来的痛苦。但模块化也不是一种万能的方法。它是一种架构原则,如果使用正确则可以在很大程度上防止上述问题的产生。

也就是说,本节中所提供的模块化定义是刻意抽象化的,这可能会让你想到基于组件的开发(20世纪曾经风靡一时)、面向服务的体系结构或当前的微服务架构。事实上,这些范例都试图在各种抽象层面上解决类似问题。

在Java中实现模块需要什么呢?建议先花时间思考一下在Java中已经存在哪些模块化的核心原则以及缺少哪些原则。

思考完了吗?如果想好了,就可以进入下一节的学习。1.2 在Java 9之前

Java可用于开发各种类型和规模的应用程序,开发包含数百万行代码的应用程序也是很常见的。显然,在构建大规模系统方面,Java已经做了一些正确的事情——即使在Java 9出现之前。让我们再来看一下Java 9模块系统出现之前Java模块化的三个核心原则。

通过组合使用包(package)和访问修饰符(比如private、protected或public),可以实现类型封装。例如,如果将一个类设置为protected,那么就可以防止其他类访问该类,除非这些类与该类位于相同的包中。但这样做会产生一个有趣的问题:如果想从组件的另一个包中访问该类,同时仍然防止其他类使用该类,那么应该怎么做呢?事实是无法做到。当然,可以让类公开,但公开意味着对系统中的所有类型都是公开的,也就意味着没有封装。虽然可以将类放置到.impl或.internal包中,从而暗示使用此类是不明智的,但谁会在意呢?只要类可用,人们就会使用它。因此没有办法隐藏这样的实现包。

在定义良好接口方面,Java自诞生以来就一直做得很好。你可能已经猜到了,我们所谈论的是Java自己的interface关键字。公开公共接口是一种经过验证的方法。它同时将实现类隐藏在工厂类后面或通过依赖注入完成。正如在本书中所看到的,接口在模块系统中起到了核心作用。

显式依赖是事情开始出问题的地方。Java确实使用了显式的import语句。但不幸的是,从严格意义上讲,这些导入是编译时结构,一旦将代码打包到JAR中,就无法确定哪些JAR包含当前JAR运行所需的类型。事实上,这个问题非常糟糕,许多外部工具与Java语言一起发展以解决这个问题。以下栏目提供了更多的细节。

用来管理依赖关系的外部工具:Maven和OSGi

Maven

使用Maven构建工具所解决的一个问题是实现编译时依赖关系管理。JAR之间的依赖关系在一个外部的POM(Project Object Model,项目对象模型)文件中定义。Maven真正成功之处不在于构建工具本身,而是生成了一个名为Maven Central的规范存储库。几乎所有的Java库都与它们的POM一起发布到Maven Central。各种其他构建工具,比如Gradle或Ant(使用Ivy)都使用相同的存储库和元数据,它们在编译时会自动解析(传递)依赖关系。

OSGi

Maven在编译时做了什么,OSGi在运行时就会做什么。OSGi要求将导入的包在JAR中列为元数据,称之为捆绑包(bundle)。此外,还必须显式定义导出哪些包,即对其他捆绑包可见的包。在应用程序开始运行时,会检查所有的捆绑包:每个导入的捆绑包都可以连接到一个导出的捆绑包吗?自定义类加载器的巧妙设置可以确保在运行时,除了元数据所允许的类型以外,没有任何其他类型加载到捆绑包中。与Maven一样,需要在JAR中提供正确的OSGi元数据。然而,通过使用Maven Central和POM,Maven取得了巨大的成功,但支持OSGi的JAR的出现却没有给人留下太深刻的印象。

Maven和OSGi构建在JVM和Java语言之上,这些都是它们所无法控制的。Java 9解决了JVM和Java语言的核心中存在的一些相同问题。模块系统并不打算完全取代这些工具,Maven和OSGi(及类似工具)仍然有自己的一席之地,只不过现在它们可以建立在一个完全模块化的Java平台之上。

就目前来看,Java为创建大型模块化应用程序提供了坚实的结构。当然,它还存在很多需要改进的地方。1.2.1 将JAR作为模块?

在Java 9出现之前,JAR文件似乎是最接近模块的,它们拥有名称、对相关代码进行了分组并且提供了定义良好的公共接口。接下来看一个运行在JVM之上的典型Java应用程序示例,研究一下JAR作为模块的相关概念,如图1-1所示。

在图1-1中,有一个名为MyApplication.jar的应用程序JAR,其中包含了自定义的应用程序代码。该应用程序使用了两个库:Google Guava和Hibernate Validator。此外,还有三个额外的JAR。这些都是Hibernate Validator的可传递依赖项,可能是由诸如Maven之类的构建工具所创建的。MyApplication运行在Java 9之前的运行时上(该运行时通过几个捆绑的JAR公开了Java平台类)。虽然Java 9之前的运行时可能是一个JRE(Java Runtime Environment,Java运行时环境)或JDK(Java Development Kit,Java开发工具包),但无论如何,都包含了rt.jar(运行时库),其中包含了Java标准库的类。图1-1:MyApplication是一个典型的Java应用程序,其打包成一个JAR文件,并使用了其他库

当仔细观察图1-1时,会发现一些JAR以斜体形式列出了相关类。这些类都是库的内部类。例如,虽然在Guava中使用了com.google.common.base.internal.Finalizer,但它并不是官方API的一部分,而是一个公共类,其他Guava包也可以使用Finalizer。不幸的是,这也意味着无法阻止com.myapp.Main使用诸如Finalizer之类的类。换句话说,没有实现强封装性。

对于Java平台的内部类来说也存在同样的情况。诸如sun.misc之类的包经常被应用程序代码所访问,虽然相关文档严重警告它们是不受支持的API,不应该使用。尽管发出了警告,但诸如sun.misc.BASE64Encoder之类的实用工具类仍然经常在应用程序代码中使用。从技术上讲,使用了这些类的代码可能会破坏Java运行时的更新,因为它们都是内部实现类。缺乏封装性实际上迫使这些类被认为是“半公共”API,因为Java高度重视向后兼容性。这是由于缺乏真正的封装所造成的结果。

什么是显式依赖呢?你可能已经看到,从严格意义上讲,JAR不包含任何依赖信息。按照下面的步骤运行MyApplication:java -classpath lib/guava-19.0.jar:\ lib/hibernate-validator-5.3.1.jar:\ lib/jboss-logging-3.3.0Final.jar:\ lib/classmate-1.3.1.jar:\ lib/validation-api-1.1.0.Final.jar \ -jar MyApplication.jar

正确的类路径都是由用户设置的。由于缺少明确的依赖关系信息,因此完成设置并非易事。1.2.2 类路径地狱

Java运行时使用类路径(classpath)来查找类。上面的示例运行了Main,所有从该类直接或间接引用的类都需要加载。可以将类路径视为可能在运行时加载的所有类的列表。虽然在“幕后”还有更多的内容,但是以上观点足以说明类路径所存在的问题。

为MyApplication所生成的类路径简图如下所示:java.lang.Objectjava.lang.String...sun.misc.BASE64Encodersun.misc.Unsafe...javax.crypto.Cypherjavax.crypto.SecretKey...com.myapp.Main...com.google.common.base.Joiner...com.google.common.base.internal.Joinerorg.hibernate.validator.HibernateValidatororg.hibernate.validator.constraints.NotEmpty...org.hibernate.validator.internal.engine.ConfigurationImpl...javax.validation.Configurationjavax.validation.constraints.NotNull

此时,没有JAR或逻辑分组的概念了。所有类按照-classpath参数定义的顺序排列成一个平面列表。当JVM加载一个类时,需要按照顺序读取类路径,从而找到所需的类。一旦找到了类,搜索就会结束并加载类。

如果在类路径中没有找到所需的类又会发生什么情况呢?此时会得到一个运行时异常。由于类会延迟加载,因此一些不幸的用户在首次运行应用程序并点击一个按钮时会出现找不到类的情况。JVM无法在应用程序启动时有效地验证类路径的完整性,即无法预先知道类路径是否是完整的,或者是否应该添加另一个JAR。显然,这并不够好。

当类路径上有重复类时,则会出现更为隐蔽的问题。假设尝试避免手动设置类路径,而是由Maven根据POM中的显式依赖信息构建将放到类路径中的JAR集合。由于Maven以传递的方式解决依赖关系问题,因此在该集合中出现相同库的两个版本(如Guava 19和Guava 18)是非常常见的,虽然这并不是你的过错。现在,这两个库JAR以一种未定义的顺序压缩到类路径中。库类的任一版本都可能会被首先加载。此外,有些类还可能会使用来自(可能不兼容的)其他版本的类。此时就会导致运行时异常。一般来说,当类路径包含两个具有相同(完全限定)名称的类时,即使它们完全不相关,也只有一个会“获胜”。

现在你应该明白为什么类路径地狱(classpath hell,也被称为JAR地狱)在Java世界如此臭名昭著了。一些人通过不断地摸索,逐步完善了调整类路径的方法——但该过程是相当艰苦的。脆弱的类路径仍然是导致问题和失败的主要原因。如果能够在运行时提供更多关于JAR之间关系的信息,那就最好了,就好像是一个隐藏在类路径中并等待被发现和利用的依赖关系图。接下来学习Java 9模块!1.3 Java 9模块

到目前为止,我们已经全面了解了当前Java在模块化方面的优势和局限。随着Java 9的出现,可以使用一个新的工具——Java模块系统来开发结构良好的应用程序。在设计Java平台模块系统以克服当前所存在的局限时,主要设定了两个目标:

·对JDK本身进行模块化。

·提供一个应用程序可以使用的模块系统。

这两个目标是紧密相关的。JDK的模块化可通过使用与应用程序开发人员在Java 9中使用的相同的模块系统来实现。

模块系统将模块的本质概念引入Java语言和运行时。模块既可以导出包,也可以强封装包。此外,它们显式地表达了与其他模块的依赖关系。可以看到,Java模块系统遵循了模块的三个核心原则。

接下来回到前面的MyApplication示例,此时示例建立在Java 9模块系统之上,如图1-2所示。图1-2:建立在模块化Java 9之上的模块化应用程序MyApplication

每个JAR都变成了一个模块,并包含了对其他模块的显式引用。hibernate-validator的模块描述符表明了该模块使用了jboss-logging、classmate和validation-api。一个模块拥有一个可公开访问的部分(位于顶部)以及一个封装部分(位于底部,并以一个挂锁表示)。这也就是为什么MyApplication不能再使用Guava的Finalizer类的原因。通过图1-2,会发现MyApplication也使用了validation-api来注释它的类。此外,MyApplication还显式依赖JDK中的一个名为java.sql的模块。

相比于图1-1所示的类路径图,图1-2告诉了我们关于应用程序的更多信息。可以这么说,就像所有Java应用程序一样,MyApplication使用了来自rt.jar的类,并且运行了(可能不正确的)该类路径上的一堆JAR。

这只是在应用层。模块的概念一直向下延伸,在JDK层也使用了模块(图1-2显示了一个小的子集)。与应用层中的模块一样,JDK层中的模块也具有显式依赖,并在隐藏了一些包的同时公开了另一些包。在模块化JDK中,最基本的平台模块是java.base。它公开了诸如java.lang和java.util之类的包,如果没有这些包,其他模块什么也做不了。由于无法避免使用这些包中的类型,因此每个模块毫无疑问都需要java.base。如果应用程序模块需要任何来自java.base之外的平台模块的功能,那么这些依赖关系也必须是显式的,就像MyApplication对java.sql的依赖一样。

最后,使用一种方法可以在Java语言的更高级别的粒度上表示代码不同部分之间的依赖关系。现在想象一下在编译时和运行时获得所有这些信息所带来的优势。这可以防止对来自其他非引用模块的代码的意外依赖。通过检查(传递)依赖关系,工具链可以知道运行模块需要哪些附加模块并进行优化。

现在,强封装性、定义良好的接口以及显式依赖已经成为Java平台的一部分。总之,Java平台模块系统带来了如下最重要的好处:1.可靠的配置

在编译或运行代码之前,模块系统会检查给定的模块组合是否满足所有依赖关系,从而导致更少的运行时错误。2.强封装型

模块显式地选择了向其他模块公开的内容,从而防止对内部实现细节的意外依赖。3.可扩展开发

显式边界能够让开发团队并行工作,同时可创建可维护的代码库。只有显式导出的公共类型是共享的,这创建了由模块系统自动执行的边界。4.安全性

在JVM的最深层次上执行强封装,从而减少Java运行时的攻击面,同时无法获得对敏感内部类的反射访问。5.优化

由于模块系统知道哪些模块是在一起的,包括平台模块,因此在JVM启动期间不需要考虑其他代码。同时,其也为创建模块分发的最小配置提供了可能性。此外,还可以在一组模块上应用整个程序的优化。在模块出现之前,这样做是非常困难的,因为没有可用的显式依赖信息,一个类可以引用类路径中任何其他类。

在下一章,将通过查看JDK中的模块,学习如何定义模块以及使用哪些概念管理模块之间的交互。JDK包含了如图1-2所示更多的平台模块。

在第2章研究模块化JDK是了解模块系统概念的一个非常好的方法,同时还可以熟悉JDK中的模块。毕竟我们将首先使用这些模块创建自己的模块化Java 9应用程序。随后,在第3章我们将准备开始编写自己的模块。第2章模块和模块化JDK

Java有超过20年的发展历史。作为一种语言它仍然很受欢迎,这表明Java一直保持很好的状态。只要查看一下标准库,就会很明显地看到该平台长期的演变过程。在Java模块系统之前,JDK的运行时库由一个庞大的rt.jar所组成(如前一章的图1-1所示),其大小超过60MB,包含了Java大部分运行时类:即Java平台的最终载体。为了获得一个灵活且符合未来发展方向的平台,JDK团队着手对JDK进行模块化——考虑到JDK的规模和结构,不得不说这是一个雄心勃勃的目标。在过去20年里,增加了许多API,但几乎没有删除任何API。

以CORBA为例,它曾经被认为是企业计算的未来,而现在是一种被遗忘的技术(对于那些仍然在使用CORBA的人,我们深表同情)。如今,JDK的rt.jar中仍然存在支持CORBA的类。无论运行什么应用程序,Java的每次发布都会包含这些CORBA类。不管是否使用CORBA,这些类都在那里。在JDK中包含这些遗留类会浪费不必要的磁盘空间、内存和CPU时间。在使用资源受限设备或者为云创建小容器时,这些资源都是供不应求的。更不用说开发过程中在IDE自动完成和文档中出现这些过时类所造成的认知超载(cognitive overhead)。

但是,从JDK中删除这些技术并不是一个可行的办法。向后兼容性是Java最重要的指导原则之一,而移除API会破坏长久以来形成的向后兼容性。虽然这样做只会影响一小部分用户,但仍然有许多人在使用CORBA之类的技术。而在模块化JDK中,不使用CORBA的人可以选择忽略包含CORBA的模块。

另外,主动地弃用那些真正过时的技术也是可行的。只不过在JDK删除这些技术之前,可能还会再发布多个主要版本。此外,决定什么技术是真正过时的取决于JDK团队,这是一个非常难做的决定。具体到CORBA,该模块被标记为已弃用,这意味着它将有可能在随后主要的Java版本中被删除。

但是,分解整体JDK的愿望并不仅仅是删除过时的技术。很多技术对于某些类型的应用程序来说是有用的,而对于其他应用程序来说是无用的。JavaFX是继AWT和Swing之后Java中最新的用户界面技术。这些技术当然是不能删除的,但显然它们不是每个应用程序都需要的。例如,Web应用程序不使用Java中任何GUI工具包。然而,如果没有这些GUI工具包,就无法完成部署和运行。

除了便利与避免浪费之外,还要从安全角度进行考虑。Java在过去经历过相当多的安全漏洞。这些漏洞都有一个共同的特点:不知何故,攻击者可以绕过JVM的安全沙盒并访问JDK中的敏感类。从安全的角度来看,在JDK中对危险的内部类进行强封装是一个很大的改进。同时,减少运行时中可用类的数量会降低攻击面。在应用程序运行时中保留大量暂时不使用的类是一种不恰当的做法。而通过使用模块化JDK,可以确定应用程序所需的模块。

目前可以清楚地看到:极需要一种对JDK本身进行模块化的方法。2.1 模块化JDK

迈向模块化JDK的第一步是在Java 8中采用了紧凑型配置文件(compact profile)。配置文件定义了标准库中可用于针对该配置文件的应用程序的一个包子集。假设定义了三个配置文件,分别为compact1、compact2和compact3。每个配置文件都是前一个配置文件的超集,添加了更多可用的包。使用这些预定义配置文件更新Java编译器和运行时。Java SE Embedded 8(仅针对Linux)提供了与紧凑型配置文件相匹配的占用资源少的运行时。

如果你的应用程序符合表2-1中所描述的其中一个配置文件,那么就可以在一个较小的运行时上运行。但是,如果需要使用预定义配置文件之外的类,那么就比较麻烦了。从这个意义上讲,紧凑型配置文件的灵活性非常差,并且也无法解决强封装问题。作为一种中间解决方案,紧凑型配置文件实现了它的目的,但最终需要一种更灵活的办法。表2-1:为Java8定义的配置文件

从图1-2中可以看到JDK 9是如何被拆分为模块的。目前,JDK由大约90个平台模块组成,而不是一个整体库。与可由自己创建的应用程序模块不同的是,平台模块是JDK的一部分。从技术上讲,平台模块和应用模块之间没有任何技术区别。每个平台模块都构成了JDK的一个定义良好的功能块,从日志记录到XML支持。所有模块都显式地定义了与其他模块的依赖关系。

图2-1显示了这些平台模块的子集及其依赖关系。每条边表示模块之间的单向依赖关系(稍后介绍实线和虚线之间的区别)。例如,java.xml依赖于java.base。如1.3节所述,每个模块都隐式依赖于java.base。在图2-1中,只有当java.base是给定模块的唯一依赖项时,才会显示这个隐式依赖关系,比如java.xml。

尽管依赖关系图看起来有点让人无所适从,但是可以从中提取很多信息。只需观察一下该图,就可以大概了解Java标准库所提供的功能以及各项功能是如何关联的。例如,java.logging有许多传入依赖项(incoming dependencies),这意味着许多其他平台模块使用了该模块。对于诸如日志之类的中心功能来说,这么做是很有意义的。模块java.xml.bind(包含用于XML绑定的JAXB API)有许多传出依赖项(outgoing dependencies),包括java.desktop(这是一个意料之外的依赖项)。事实上,我们通过查看生成的依赖关系图并进行讨论而发现这个奇异之处,这本身就是一个巨大的进步。由于JDK的模块化,形成了清晰的模块边界以及显式的依赖关系。根据显式模块信息了解JDK之类的大型代码块是非常有价值的。

另一个需要注意的是,依赖关系图中的所有箭头都是向下的,图中没有循环。这是必然的:Java模块系统不允许模块之间存在编译时循环依赖。循环依赖通常是一种非常不好的设计。在5.5.2节中,将讨论如何识别和解决代码库中的循环依赖。

除了jdk.httpserver和jdk.unsupported之外,图2-1中的所有模块都是Java SE规范的一部分。它们的模块名都共享了前缀java.*。每个认证的Java实现都必须包含这些模块。诸如jdk.httpserver之类的模块包含了工具和API的实现,虽然这些实现不受Java SE规范的约束,但是这些模块对于一个功能完备的Java平台来说是至关重要的。JDK中还有很多的模块,其中大部分在jdk.*命名空间。通过运行java--list-modules,可以获取平台模块的完整列表。图2-1:JDK平台模块的子集

在图2-1的顶部可以找到两个重要的模块:java.se和java.se.ee。它们就是所谓的聚合器模块(aggregator module),主要用于对其他模块进行逻辑分组。本章稍后将介绍聚合器模块的工作原理。

将JDK分解成模块需要完成大量的工作。将一个错综复杂、有机发展且包含数以万计类的代码库分解成边界清晰且保持向后兼容性的定义良好的模块需要花费大量的时间,这也就是为什么花了如此长的时间才将模块系统植入到Java中的原因。经过20多年的传统积累,许多存在疑问的依赖关系已经解开。展望未来,这一努力将在JDK的快速发展以及更大灵活性方面得到丰厚的回报。

孵化器模块

模块所提供的另一个改进示例是JEP 11(http://openjdk.java.net/jeps/11JEP表示Java Enhancement Proposal)中所描述的孵化器模块(incubator module)概念。孵化器模块是一种使用JDK提供实验API的手段。例如,在使用Java 9时,jdk.incubator.httpclient模块中提供了一个新的HttpClient API(所有孵化器模块都具有jdk.incubator前缀)。如果愿意,可以使用这样的孵化器模块,并且明确地知道模块中API仍然可以更改。这样一来,就可以让这些API在真实环境中不断变得成熟和稳定,以便在日后的JDK版本中可以作为一个完全支持模块来使用或者删除(如果API在实践中不成功)。2.2 模块描述符

到目前为止,我们已经大概了解了JDK模块结构,接下来探讨一下模块的工作原理。什么是模块,它是如何定义的?模块拥有一个名称,并对相关的代码以及可能的其他资源进行分组,同时使用一个模块描述符进行描述。模块描述符保存在一个名为module-info.java的文件中。示例2-1显示了java.prefs平台模块的模块描述符。

示例2-1:module-info.java

①关键字requires表示一个依赖关系,此时表示对java.xml的依赖。

②来自java.prefs模块的单个包被导出到其他模块。

模块都位于一个全局命名空间中,因此,模块名称必须是唯一的。与包名称一样,可以使用反向DNS符号(例如com.mycompany.project.somemodule)等约定来确保模块的唯一性。模块描述符始终以关键字module开头,后跟模块名称。而module-info.java的主体描述了模块的其他特征(如果有的话)。

接下来看一下java.prefs模块描述符的主体。java.prefs使用了java.xml中的代码从,以XML文件中加载首选项。这种依赖关系必须在模块描述符中表示。如果没有这个依赖关系声明,模块系统就无法编译(或运行)java.prefs模块。声明依赖关系首先使用关键字requires,然后紧跟模块名称(此时为java.xml)。可以将对java.base的隐式依赖添加到模块描述符中。但这样做没有任何价值,就好比是将“import java.lang.String”添加到使用字符串的类中(通常并不需要这么做)。

模块描述符还可以包含exports语句。强封装性是模块的默认特性。只有当显式地导出一个包时(比如示例中的java.util.prefs),才可以从其他模块中访问该包。默认情况下,一个模块中若没有导出的包则无法被其他模块所访问。其他模块不能引用封装包中的类型,即使它们与该模块存在依赖关系。从图2-1中可以看到,java.desktop依赖java.prefs,这意味着java.desktop只能访问java.prefs模块的java.util.prefs包中的类型。2.3 可读性

在推理模块之间的依赖关系时,需要注意的一个重要的新概念是可读性(readability),读取其他模块意味着可以访问其导出包中的类型。可以在模块描述符中使用requires子句设置模块之间的可读性关系。根据定义,每个模块都可以读取自己。而一个模块之所以读取另一个模块是因为需要该模块。

接下来,再次查看java.prefs模块,了解一下可读性的影响。在示例2-2的JDK模块中,XmlSupport类导入并使用了java.xml模块中的类。

示例2-2:类java.util.prefs.XmlSupport的节选import org.w3c.dom.Document;// ...class XmlSupport { static void importPreferences(InputStream is) throws IOException, InvalidPreferencesFormatException { try { Document doc = loadPrefsDoc(is); // ... } } // ...}

此时,导入了org.w3c.dom.Document(以及其他类),该类来自java.xml模块。如示例2-1所示,由于java.prefs模块描述符包含了requires java.xml,因此代码可以顺利完成编译。如果java.prefs模块的作者省略了require子句,那么Java编译器将报告错误。在模块java.prefs中使用来自java.xml的代码是一个经过深思熟虑并显式记录的选择。

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

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载