龙空技术网

“旧Java”的日子已经一去不复返了

图灵教育 7682

前言:

眼前大家对“apachefelixss”大体比较关怀,你们都需要剖析一些“apachefelixss”的相关内容。那么小编同时在网络上搜集了一些有关“apachefelixss””的相关内容,希望各位老铁们能喜欢,兄弟们快快来了解一下吧!


简单地说,Java 8中的新增功能以及Java 9引入的变化(虽然并不显著)是自Java 1.0发布21年以来,Java发生的最大变化。这一演进没有去掉任何东西,因此你原有的Java代码都能工作,但新功能提供了更强大的新习语和新设计模式,能帮助你编写更清晰、更简洁的代码。就像遇到所有新功能时那样,你一开始可能会想:“为什么又要去改我的语言呢?”但稍加练习之后,你就会发觉自己只用预期的一半时间,就用新功能写出了更短、更清晰的代码,这时你会意识到自己永远无法返回到“旧Java”了。

全新升级的它全面介绍了Java 8、9、10的版本新特性,包括Lambda表达式、方法引用、流、默认方法、Optional、CompletableFuture以及新的日期和时间API,是程序员了解Java新特性的经典指南。全书共分六个部分:基础知识、使用流进行函数式数据处理、使用流和Lambda进行高效编程、无所不在的Java、提升Java的并发性、函数式编程以及Java未来的演进。

一、Java模块系统

Java 9中引入的最主要并且讨论最多的新特性无疑是它的模块系统。模块系统诞生于Jigsaw项目,它的开发持续了将近十年。从时间线就可以一瞥这个特性的重要性以及研发团队在开发过程中所经历的挑战。本章会介绍开发者为什么需要关注模块系统,并提纲挈领地介绍新的Java模块系统试图解决哪些问题以及你能从中得到哪些好处。

注意,Java的模块系统是个非常复杂的话题,深入讨论它可能需要写一本书。如果想全面了解Java模块系统,建议你阅读一下Nicolai Parlog的著作The Java Module System。本章会刻意避免深究模块系统繁杂的细节,旨在让你大致理解模块系统诞生的缘由及其使用方法。

1. 模块化的驱动力:软件的推理

学习Java模块系统的各种细节之前,如果你能理解Java语言设计者设计的初衷和背景将会大有裨益。模块化意味着什么?模块系统能解决什么问题?本书花了大量的篇幅讨论新语言特性如何帮助程序员编写更接近问题描述的代码,以使代码更易于理解和维护。然而,这些都是底层的考虑。最终你需要从更高的层次(软件架构的层面)去设计,确保软件项目易于理解,进行代码变更时更加灵活、高效。接下来,我们会着重讨论两个设计模式,即关注点分离(separation of concern,SoC)和信息隐藏(information hiding),它们可以帮助创建易于理解的软件。

(1) 关注点分离

关注点分离推崇的原则是将单体的计算机程序分解为一个个相互独立的特性。譬如你要开发一个结算应用,它需要能解析各种格式的开销,能对结果进行分析,进而为顾客提供汇总报告。采用关注点分离,你可以将文件的解析、分析以及报告划分到名为模块的独立组成部分。模块是具备内聚特质的一组代码,它与其他模块代码之间很少有耦合。换句话说,通过模块组织类,可以帮助你清晰地表示应用程序中类与类之间的可见性关系。

你可能会质疑:“Java通过包机制不是已经对类进行了组织吗?为什么还需要模块?”你说得没错,不过Java 9的模块能提供粒度更细的控制,你可以设定哪个类能够访问哪个类,并且这种控制是编译期检查的。而Java的包并未从本质上支持模块化。

无论是从架构角度(比如,模型–视图–控制器模式)还是从底层实现方法(比如,业务逻辑与恢复机制的分离)而言,关注点分离都非常有价值。它能带来的好处包括:

□ 使得各项工作可以独立开展,减少了组件间的相互依赖,从而便于团队合作完成项目;

□ 有利于推动组件重用;

□ 系统整体的维护性更好。

(2)信息隐藏

信息隐藏原则要求大家设计时尽量隐藏实现的细节。这一原则为什么非常重要呢?创建软件的过程中,我们经常遭遇需求变更的窘境。隐藏内部实现细节能帮你减少局部变更对程序其他部分的影响,从而有效地避免变更传递。换句话说,这是一种非常有用的代码管理和保护原则。我们经常听到封装这个词,意指一段代码的设计实现非常精巧,与应用的其他部分没有任何耦合,对这段代码内部实现的更迭不会对应用的其他部分产生影响。Java语言中,你可以通过private关键字,借助编译器验证组件中的类是否封装良好。不过,就语言层面而言,Java 9出现之前,编译器无法依据语言结构判断某个类或者包仅供某个特定目标访问。

(3) Java软件

任何设计良好的软件都基于上述两个重要原则。那么,如何在Java语言中应用这两个原则呢?Java是一种面向对象的语言,我们日常打交道面对的都是类和接口。按照要解决的问题,对包、类以及接口代码进行分组,完成程序的模块化。实际操作时,以源代码方式展开分析可能过于抽象。你可以借助UML图这样的工具,以可视化的方式理解代码间的依赖。图14-1是一个UML图示例。这是一个管理用户注册信息的应用,它被分解成了三个独立的模块。

信息隐藏原则又该如何实现呢?你应该很熟悉Java语言的可见性描述符,它可以指定方法、字段以及类的访问控制,譬如:public、protected、包访问权限(package-level)或者是private。不过,正如下一节中将要提到的,这种方式提供的颗粒度很多情况下比较粗,即便你不希望用户能直接访问某个方法,可能还是不得不将其声明为public。在Java发展的早期,这并不是一个非常致命的问题,因为那时的应用规模比较小,依赖也相对简单。而现在,很多Java应用的规模都比较庞大,这个问题的严重程度日益凸显。事实上,如果你看到类中某个字段或者方法声明为public,就会下意识地觉得可以直接使用(难道不是吗?),然而这些方法设计者的初衷是它们只应该被他自己创建的有限类所访问!


图14-1 三个独立的模块及它们之间的依赖

现在你应该已经理解模块化能带来的好处,甚至开始思考模块化对Java产生了哪些变化。接下来将围绕这一主题继续展开讨论。

2. 为什么要设计Java模块系统

这一节里,你会了解为什么Java语言及其编译器需要一个全新的模块系统。首先,我们会介绍Java 9之前版本在模块化方面的局限性。接着,我们会聊聊JDK库的一些背景知识并解释为什么模块化如此重要。

(1) 模块化的局限性

不幸的是,Java 9之前内建的模块化支持或多或少都存在一些局限,无法有效地实现软件项目的模块化。从代码层次而言,Java的模块化可以分为三层,分别是:类、包以及JAR。对类而言,Java可以通过访问修饰符实现封装。不过,从包和JAR的层次看,对应的封装则相当有限。

有限的可见性控制

正如前文所述,Java提供了访问描述符来支持信息封装。这些描述符可以设定对象的公有访问、保护性访问、包级别访问以及私有访问。不过,如果需要控制包之间的访问,又该如何做呢?大多数Java应用程序采用包来组织和管理不同的类,然而包之间的访问控制方式乏善可陈。如果你希望一个包中的某个类或接口可以被另外一个包中的类或接口访问,那么只能将它声明为public。这样一来,任何人都可以访问这些类和接口了。这种问题的典型症状是,你能直接访问包中名字含有impl字符串的类——这些类通常用于提供某种默认实现。由于包内的这段代码被声明为公有访问,因此你无法限制其他人访问或者使用这些内部实现。这样一来,你的代码演进就受到了极大的制约,局部代码的变更可能导致无法预计业务失效,因为你原以为仅供内部使用的类或者接口,可能会被某个程序员在编码解决某个问题时突发奇想地调用,很快这种结构不良好的代码就会融入整个系统。从安全性角度而言,这种状况带来的影响更为严重,它增大了系统受攻击的可能性,因为更多的代码都暴露在了攻击面下。

类的路径

本章前面讨论了用容易维护和理解,也就是易于推理的方式构建软件的好处。我们也探讨了关注点分离以及模块间的模型依赖(modeling dependency)。非常不幸的是,说到应用的打包以及运行,Java一直以来在这些方面都存在着短板。实际上,每次发布时你只能把所有的类打包成一个扁平结构的JAR文件,并把这个JAR包添加到类路径(class path)[①]上。这之后JVM才能按照需求动态地从类的路径中定位并载入相关的类。

然而,类路径与JAR混合使用也存在几个严重的问题。

首先,对同一个类,无法指定到底使用类路径上的哪一个版本,因为根本无法通过路径指定版本。举个例子,使用来自某个解析库的JSONParser类时,你无法指定是使用1.0版本的还是2.0版本的,由此也无法预测,如果类路径上同一个库存在两个不同版本会发生什么。这种情况在大型应用中相当常见,因为应用的不同组件可能需要使用同一个库的不同版本。

其次,类路径也不支持显式的依赖。类路径上林林总总的JAR中所有的类都被一股脑地塞到了一个类组成的大包裹中。换句话说,类路径不支持显式地声明某个JAR依赖于另一个JAR中的某些类。这种设计使得我们很难对类路径进行分析并回答下面这种问题,譬如:

□ 是否有某些类在路径中遗漏了?

□ 路径上的类是否存在冲突?

Maven或者Gradle这样的构建工具可以帮助解决这一问题。Java 9之前,无论是Java还是JVM都不支持显式地声明依赖。这些问题碰到一起就产生了我们称之为“JAR地狱”或“类路径地狱”的问题。这些问题的直接结果就是我们不停地在类路径上添加和删除类文件,希望能通过实验找出合适的搭配,让JVM顺利地执行应用,不再抛出让人头疼的ClassNotFound Exception。理想情况下,这种问题在开发的早期阶段就应该被发现并解决。好消息是,如果你持续一致地在项目中使用Java 9的模块系统,刚才提到的所有问题都可以在编译期就被捕获。

像“类路径地狱”这样的封装问题并不是只存在于你的软件架构中,JDK自身也存在类似的问题。

① 这种说法常用于Java 文档,对于程序参数而言,常使用的是classpath。

(2) 单体型的JDK

Java开发工具集(JDK)由一系列编写并执行Java程序的工具组成。有几个重要的工具你可能已经很熟悉了,譬如,javac可以编译Java程序,而java搭配JDK提供的库可以加载并执行Java应用。JDK库提供了Java程序的运行时支持,包括输入/输出、集合以及流。第一版JDK发布于1996年。像任何其他的软件一样,随着新特性的引入,JDK也不断增大,理解这一点非常重要。许多之前加入的技术随着潮流的更迭逐渐被废弃。这其中一个著名的例子就是CORBA。无论你是否在你的应用中使用了CORBA,对CORBA的支持默认都打包在JDK之中。由于越来越多的应用运行在移动设备或者云端,它们通常不需要JDK中所有的内容,因此之前这种打包发布模式问题的影响就变得越来越严重了。

怎样从全局或者整个系统的角度来解决这一问题呢?Java 8引入了精简配置(compact profile)这一概念,这是一个很好的尝试。Java 8定义了三种配置,它们的内存开销不一样,你可以根据应用需要的到底是JDK库的哪一部分来决定使用哪一个配置。然而,精简配置只是一个短期的解决方案。JDK中存在着大量的内部API,这些内部API并不是为普通用户使用所设计的。不幸的是,由于Java语言糟糕的封装,这些API现在被大量地使用了。一个典型的例子是sun.misc.Unsafe类,这个类被好几个流行的类库(包括Spring、Netty、Mockito等)所使用,不过它设计之初并不期望被JDK之外的任何代码访问或使用。由于这些牵绊,想要改进这些API非常困难,因为结果很可能是牵一发而动全身,引起前后不兼容的问题。

这些问题为设计新的Java模块系统提供了动力,反过来也用在了JDK自身的模块化上。简而言之,新的结构让你可以更灵活地选择使用JDK的哪一部分以及如何规划类路径,同时也为Java平台的进一步发展演化提供了更强大的封装。

(3) 与OSGi的比较

本节会比较Java 9的模块系统与OSGi。如果你从未听说过OSGi,那建议你跳过本节的内容。

Java 9基于Jigsaw项目引入的模块系统诞生之前,Java已经有了一个比较强大的模块系统,名叫开放服务网关协议(open service gateway initiative,OGSi),不过它并非Java平台的官方组成部分。OSGi最早提出于2000年,直到Java 9诞生,一直都是实现基于JVM的模块化应用的事实标准。

实际上,OGSi与新的Java 9模块系统之间并不是完全互斥的,它们甚至可以在同一个应用之中共存。事实上,它们的特性只有小部分的重叠。OGSi所覆盖的范畴要大得多,很多的功能迄今为止在Jigsaw中还不支持。

在OGSi中,模块被称作bundle,它们运行在某个OGSi的框架之中。市面上有多个OGSi认证支持的框架,应用最广的两个是Apache Felix和Equinox(也被用于执行Eclipse的集成开发环境)。一个bundle运行于OGSi框架中时,它可以被远程安装、启动、停止、更新以及卸载,任何一个动作都无须重启应用。换句话说,OGSi为bundle定义了一个非常清晰的生命周期,其状态如表14-1所示。


与Jigsaw相比,能够以热切换方式替换应用的各个子系统而无须重启应用是OGSi最大的优势。每一个bundle都通过文本文件声明了该bundle运行所需的外部包依赖,以及由这个bundle导出并可以被其他bundle使用的内部包。

OGSi的另一个有趣的特性是,它允许在框架中同时安装同一个bundle的不同版本。Java 9模块系统还不支持这样的版本控制,因为Jigsaw中每个应用仅使用一个类加载器,而OGSi中每一个bundle都有单独的类加载器。

3. Java模块:全局视图

Java 9为Java程序提供了一个新的单位:模块。模块通过一个新的关键字[①]module声明,紧接着是模块的名字及它的主体。这样的模块描述符(module descriptor)[②]定义在一个特殊文件,即module-info.java中,最终被编译为module-info.class。模块描述符的主体包含一系列的子句,其中最重要的两个子句是requires和exports。requires子句用于指定执行你的模块还需要哪些模块的支持,exports子句声明了你的模块中哪些包可以被其他模块访问和使用。本节稍后会详细介绍如何使用这些子句。

模块描述符描述和封装了一个或多个包(通常它跟这些包都位于同一个目录中),但是在简单的用例中,可以只导出这些包中的一个(即使其可见于其他模块)。

Java模块描述符的核心结构如图14-2所示。


① 严格来说,Java 9 的模块标识符,比如module、requires 和export,都是受限关键字。然而你还是可以在程序中将它们作为标识符使用(出于后向兼容性的考虑),不过它们在允许模块出现的上下文中会被解释成关键字。


② 从约定上来说,文本形式应该被称为模块声明,module-info.class 中的二进制形式才应该被称为模块描述符。

将模块中的exports和requires看作相互独立的部分,就像拼图游戏(这可能也是Jigsaw项目名称的起源)中的凸块(或者标签)与凹块的关系,对理解模块是非常有益的。图14-3展示了使用多个模块的一个例子。


当你使用Maven这样的构建工具时,模块描述之类的细节都被集成开发环境解决了,用户也就看不到这些琐碎的事情了。

话虽如此,下一节会结合例子详细探讨刚才介绍的这些概念。

4. 使用Java模块系统开发应用

本节会介绍如何从零开始构建一个简单的模块化应用,从而让你对Java 9模块系统有一个全局的认识。你会学到如何构架、打包以及发布一个小型模块化应用。本节不会深入到模块化每一方面的细节,不过一旦有了全局的视图,需要的时候你可以在此基础上做进一步的研究。

(1) 从头开始搭建一个应用

为了开始使用Java模块系统,你需要一个示例项目才能着手编写代码。我们假设你爱旅行,爱去超市购物,也爱跟朋友一起去咖啡店闲谈聊天,为此你需要处理大量的发票。大家都不喜欢管理开支。为了解决这个问题,你决定编写一个程序来管理自己的开支。这个应用需要有能力完成下面这些任务:

□ 从一个文件或者URL中读取开支列表;

□ 解析出代表开支的字符串;

□ 计算统计数据;

□ 展示一个有价值的汇总信息;

□ 提供一个总控方法,统一协调这些任务的启动或者停止。

你需要定义各种类和接口来对应用中的概念进行建模。首先,你需要定义一个Reader接口以序列化的方式读取来自源头的数据。依据数据源的不同,你需要定义不同的实现,比如HttpReader或者FileReader。你还需要定义一个Parser接口,用于反序列化JSON对象,将它们转换为领域对象Expense,你的应用会对这些转换后的Expense对象进行相应的处理。最后,你还需要一个SummaryCalculator类负责数据的统计工作,它接受一个Expense对象的列表,返回一个SummaryStatistics对象。

至此,你已经有了一个项目需求,那么怎样利用Java模块系统对这些需求进行模块化呢?很明显,这个项目有几个关注点,你需要对它们分别进行处理:

□ 从不同的数据源读取数据(Reader、HttpReader、FileReader);

□ 从不同的格式中解析数据(Parser、JSONParser、ExpenseJSONParser);

□ 表示领域对象(Expense);

□ 计算并返回统计数据(SummaryCalculator、SummaryStatistics);

□ 协调各个任务的处理(ExpensesApplication)。

出于教学的目的,这里会采用细粒度的方法。你可以将这些关注点切分到不同的模块中,如下所示(后续会深入讨论模块命名方案):

□ expenses.readers

□ expenses.readers.http

□ expenses.readers.file

□ expenses.parsers

□ expenses.parsers.json

□ expenses.model

□ expenses.statistics

□ expenses.application

在这个简单的例子中,我们采用的模块分解粒度很细,主要目的在于介绍模块系统的各个部分。在实际操作中,对于这种简单的项目如果也采用这么细粒度的划分,会导致前期的成本过高,付出这么高的代价却只对项目少部分的内容进行了恰当的封装。随着项目的不断演进,更多的内部实现被加入进来,这时封装和划分的价值就变得越来越明显。你可以将前述的列表想象成一个由包组成的列表,它的长度取决于你的应用边界。模块对一系列的包进行组织。有可能应用的每个模块都包含一些依赖特定实现的包,你不希望将这些包泄露给其他的模块使用。 譬如,在expenses.statistics模块中,针对不同的实验统计方法可能就采用了不同实现的包。 稍后,你可以决定将这些包中的哪些发布给用户。

(2) 细粒度和粗粒度的模块化

当你开始模块化一个系统的时候,可以选择以怎样的粒度进行模块化。最细粒度的方法是让每个包都独立拥有一个模块(就像上一节介绍的那样);最粗粒度的方法是把所有的包都归属到一个单一模块中。前一节已经介绍过,第一种策略极大地增加了设计的开销,并且获得的收益有限;第二种策略则完全牺牲了模块化能带来的好处。最好的选择是根据实际需求将系统分解到各个模块中并定期进行评审,从而确保随着软件项目的不断演进,代码的模块化还能保持其效果,你可以很清晰地厘清其脉络并进行修改。

简而言之,模块化是对抗软件腐臭的利器。

(3) Java模块系统基础

我们从一个基础的模块应用开始介绍,这个应用只有一个模块供main应用调用。项目的目录结构如下所示,每一层目录以递归的方式嵌套:

|─ expenses.application    |─ module-info.java    |─ com        |─ example            |─ expenses                |─ application                     |─ ExpensesApplication.java

大概你已经注意到了,项目结构中也包含了那个神秘的module-info.java文件。本章前面介绍过,这个文件是一个模块描述符,它必须位于模块源码文件目录结构的根目录,通过它你可以指定你的模块依赖以及希望导出哪些包给别的模块使用。对你的开支管理应用而言,module-info.java文件的顶层模块描述部分只有一个名字,其他都是空的,因为它既不依赖于其他的模块,也不需要导出它的功能给别的模块使用。14.5节会进一步学习模块更复杂的特性。本例中module-info.java的内容如下:

module expenses.application {}

如何运行一个模块化的应用呢?让我们查看几个命令来理解一下底层的机制。这部分的代码都是由你的集成开发环境和编译系统完成的,不过了解一下到底发生了什么还是非常有价值的。进入项目的模块源码目录后,你可以执行下面的命令:

javac module-info.java      com/example/expenses/application/ExpensesApplication.java -d targetjar cvfe expenses-application.jar     com.example.expenses.application.ExpensesApplication -C target


执行这些命令的输出就类似下面这样,其显示了哪些目录和类文件会被打包进入生成的JAR(expenses-application.jar)文件中:

added manifestadded module-info: module-info.classadding: com/(in = 0) (out= 0)(stored 0%)adding: com/example/(in = 0) (out= 0)(stored 0%)adding: com/example/expenses/(in = 0) (out= 0)(stored 0%)adding: com/example/expenses/application/(in = 0) (out= 0)(stored 0%)adding: com/example/expenses/application/ExpensesApplication.class(in = 456)        (out= 306)(deflated 32%)

终于,你可以以模块应用的方式执行生成的JAR文件了:

java --module-path expenses-application.jar \        --module expenses/com.example.expenses.application.ExpensesApplication

刚才的这个过程,前两步你应该非常熟悉,它们是将Java应用打包到一个JAR文件中的标准方式。唯一不同的是,module-info.java文件成了编译过程的一部分。

现在Java程序执行Java的.class文件时,增加了两个新的选项。

□ --module-path——用于指定哪些模块可以加载。它与--classpath参数又不尽相同,--classpath仅是使类文件可以访问。

□ --module——指定运行的主模块和类。

模块的声明不包含版本信息。解决版本选择问题并不是Java 9模块系统设计的出发点,所以它不支持版本。做这个决定的理由是这个问题应该由编译工具和应用容器来解决。

5. 使用多个模块

你现在已经掌握了如何建立一个单模块的应用,是时候使用多个模块做一些更接近现实情况的事儿了。你想让你的开支管理应用从数据源读取数据。为了达到这个目的,需要引入一个新的模块expenses.readers,它封装了对应的操作。借助Java 9的exports和requires子句,可以对现有两个模块expenses.application和expenses.readers之间的交互进行设置。

(1) exports子句

下面是声明expenses.readers的一个示例(暂时不用担心那些看不懂的语法和概念,稍后会逐一介绍)。


这段声明中引入了一个新东西:exports子句,由它声明的这些包会变为公有类型,可以被其他模块访问和调用。默认情况下,模块中的所有内容都是被封装的。模块系统使用白名单的方式帮助你进行更严格的封装控制,因此你需要显式地声明你愿意将哪些内容提供给别的模块访问(这种方式可以避免你由于偶然的机会开放一些内部接口给外部使用,因为这些接口如果几年后被某些黑客破解,可能导致你的系统被攻破)。

你的项目现在包含了两个模块,其目录结构如下:

|─ expenses.application    |─ module-info.java    |─ com        |─ example            |─ expenses                |─ application                    |─ ExpensesApplication.java|─ expenses.readers    |─ module-info.java    |─ com        |─ example            |─ expenses                |─ readers                    |─ Reader.java                |─ file                    |─ FileReader.java                |─ FileReader.java                    |─ FileReader.java                |─ FileReader.java                 |─ FileReader.java                       |─ FileReader.java                 |─ FileReader.java|─ http |─ HttpReader.java

(2) requires子句

此外,你还可以像下面这样定义module-info.java:


这里新增的元素是requires子句,通过它你可以指定本模块对其他模块的依赖。默认情况下,所有的模块都依赖于名叫java.base的平台模块,它包含了Java主要的包,比如net、io和util。默认情况下,这个模块总是需要的,因此你不需要显式声明(这个就跟Java语言中,class Foo { ... }等价于class Foo extends Object { ... }一样)。

如果你需要导入java.base之外的其他模块,requires子句就必不可少了。

requires和exports子句的组合使得Java 9中的访问控制变得更复杂了。表14-2总结了Java 9之前与之后使用不同的访问修饰符时在对象可见性上的差异。


(3) 命名

现在是时候讨论如何命名模块了。我们会以一个比较短的名字为例进行介绍(譬如expenses.application),这样做主要是为了避免混淆模块与包的命名(一个模块可以导出多个包)。不过,对于模块和包的命名,推荐的命名规范是不一样的。

Oracle公司推荐大家在命名模块时采用与包同样的方式,即互联网域名规范的逆序(譬如,com.iteratrlearning.training)。此外,模块名应该与它导出的主要API的包名保持一致,包名也应该遵循同样的规则。如果模块中并不存在这样的包,或者出于某些别的原因模块的命名不能直接与它导出的包对应,那么这种情况下模块名也应以互联网域名规范同样的逆序方式设计,并在其中插入作者的名字。

现在你已经了解了如何构建一个多模块的项目,不过该怎样打包并运行它呢?别急,这些内容会在下一节中介绍。

6. 编译及打包

你已经掌握了如何建立项目和声明模块,接下来学习如何使用像Maven这样的构建工具编译你的项目。本节假设你已经对Maven有一定的了解,它是Java生态圈里使用最广泛的构建工具之一。除了Maven之外,另一个同样很流行的构建工具是Gradle,如果你从未听说过,建议你抽时间了解一下。

构建的第一步,你需要为每一个模块创建一个pom.xml文件。实际上,每一个模块都需要能单独编译,这样其自身才能成为一个独立的项目。你还需要为所有模块的上层父项目创建一个pom.xml,用于协调整个项目的构建。这样一来,项目的整体结构就如下所示:

|─ pom.xml|─ expenses.application    |─ pom.xml    |─ src        |─ main            |─ java                |─ module-info.java                |─ com                    |─ example                        |─ expenses                            |─ application                                |─ ExpensesApplication.java|─ expenses.readers    |─ pom.xml    |─ src        |─ main            |─ java                |─ module-info.java                |─ com                    |─ example                        |─ expenses |─ readers |─ readers |─ Reader.java |─ file |─ FileReader.java |─ http |─ HttpReader.java

请注意这三个新创建的pom.xml以及Maven项目的目录结构。项目的模块描述符(module-info.java)应置于src/main/java目录之中。Maven会设置javac,让其匹配对应模块的源码路径。

<?xml version="1.0" encoding="UTF-8"?><project xmlns=";              xmlns:xsi=";              xsi:schemaLocation="        ;>                 <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>expenses.readers</artifactId> <version>1.0</version> <packaging>jar</packaging> <parent> <groupId>com.example</groupId> <artifactId>expenses</artifactId> <version>1.0</version> </parent></project>

有一点非常重要,你需要在代码中显式地指定构建过程中使用的父模块。父模块在这个例子中是ID为expenses的构件。正如很快就能看到的例子所示,你需要在pom.xml中显式地定义父模块。

接下来,你需要指定模块expenses.application对应的pom.xml。这个文件与之前的那个pom.xml很类似,不过你还需要为其添加对expenses.readers项目的依赖,因为Expenses-
Application需要使用它提供的类和接口进行编译:

<?xml version="1.0" encoding="UTF-8"?><project xmlns=";             xmlns:xsi=";             xsi:schemaLocation="        ;>        <modelVersion>4.0.0</modelVersion>        <groupId>com.example</groupId>        <artifactId>expenses.application</artifactId>        <version>1.0</version>        <packaging>jar</packaging>        <parent>               <groupId>com.example</groupId>               <artifactId>expenses</artifactId>               <version>1.0</version>        </parent>        <dependencies>               <dependency>                               <groupId>com.example</groupId>                               <artifactId>expenses.readers</artifactId>                               <version>1.0</version>               </dependency>        </dependencies></project>

至此expenses.application和expenses.readers都有了各自的pom.xml,你可以着手建立指导构建流程的全局pom.xml了。Maven通过一个特殊的XML元素<module>支持一个项目包含多个Maven模块的情况,这个<module>定义了对应的子构件ID。下面是完整的定义,它包含了前面提到的两个子模块expenses.application和expenses.readers:

<?xml version="1.0" encoding="UTF-8"?><project xmlns=";              xmlns:xsi=";              xsi:schemaLocation="        ;>        <modelVersion>4.0.0</modelVersion>        <groupId>com.example</groupId>        <artifactId>expenses</artifactId>        <packaging>pom</packaging>        <version>1.0</version>        <modules>        <module>expenses.application</module>                  <module>expenses.readers</module>        </modules>        <build>               <pluginManagement>                      <plugins>                      <plugin>                                     <groupId>org.apache.maven.plugins</groupId>                             <artifactId>maven-compiler-plugin</artifactId>                                     <version>3.7.0</version>                             <configuration>                                            <source>9</source>                                            <target>9</target>                             </configuration>                      </plugin>                      </plugins>               </pluginManagement>        </build></project>

恭喜你!现在可以执行mvn clean package为你项目中的模块生成JAR包了。运行这条命令会产生下面的文件:

./expenses.application/target/expenses.application-1.0.jar./expenses.readers/target/expenses.readers-1.0.jar

把这两个JAR文件添加到模块路径中,你就可以运行你的模块应用了,如下所示:

java --module-path \   ./expenses.application/target/expenses.application-1.0.jar:\   ./expenses.readers/target/expenses.readers-1.0.jar \         --module \   expenses.application/com.example.expenses.application.ExpensesApplication

至此,你已经学习并创建了模块,知道如何利用requires引用java.base。然而现实生产环境中,软件依赖更多的往往是外部的模块和库。如果遗留代码库没有使用module-info.java,又该如何处理呢?下一节会通过介绍自动模块(automatic module)来回答这些问题。

7. 自动模块

你可能会觉得HttpReader的实现过于底层,希望使用其他的库,譬如Apache项目的httpclient来替换这段逻辑。怎样才能把这个库导入到你的项目中呢?还记得之前学过的requires子句吧?你可以把它加到expenses.readers项目的module-info.java中,指定需要的第三方库。再次运行mvn clean package,看看会发生什么?非常不幸,结果并不是很理想,它抛出了下面的错误:

[ERROR] module not found: httpclient

碰到这个错误的原因是你没有更新你的pom.xml,明确声明对应的依赖。Maven的编译器插件在编译使用了module-info.java的项目时会去下载对应的JAR,并将所有的依赖添加到模块路径上,从而确保在项目中能识别对应的对象,如下所示:

<dependencies>        <dependency>                <groupId>org.apache.httpcomponents</groupId>                <artifactId>httpclient</artifactId>                <version>4.5.3</version>        </dependency></dependencies>

现在你执行mvn clean package构建项目,结果就正确了。不过,你注意到一些有趣的事情了吗?httpclient库并不是一个Java模块啊。它是你希望以模块方式使用的一个第三方库,可是并没有被“模块化”过。这就是我们想特别介绍的部分,Java会将对应的JAR包转换为所谓的“自动模块”。模块路径上不带module-info.java文件的JAR都会被转换为自动模块。自动模块默认导出其所有的包。自动模块的名字会依据JAR的名字自动创建。不过你也可以通过几种途径修改它的名字,其中最简单的方式是使用jar工具提供的--describe-module参数,如下所示:

jar --file=./expenses.readers/target/dependency/httpclient-4.5.3.jar \     --describe-modulehttpclient@4.5.3 automatic

这个例子中,你把模块名改成了httpclient。

最后一步,将JAR文件httpclient添加到模块路径上,运行这个应用:

java --module-path \   ./expenses.application/target/expenses.application-1.0.jar:\   ./expenses.readers/target/expenses.readers-1.0.jar \   ./expenses.readers/target/dependency/httpclient-4.5.3.jar \         --module \   expenses.application/com.example.expenses.application.ExpensesApplication

注意 如果你使用Maven,有个名叫moditect的项目对Java 9的模块系统提供了更好的支持,譬如,它可以自动帮助用户生成module-info文件。

8. 模块声明及子句

Java模块系统非常复杂,就像一个庞然大物。之前我们也提过,如果你想进一步了解Java模块系统,建议你选择一本专门讲述相应内容的著作来深入学习。不过,我们还是希望通过这一节的概述,让你了解模块声明语言中有哪些关键字,以及它们大致能做些什么。

前文已经介绍过,你可以通过模块指令声明一个模块。就像下面这段代码,它声明了一个名为com.iteratrlearning.application的模块:

module com.iteratrlearning.application {}

模块声明内部有什么?你已经学习了requires和exports子句,但是模块还提供了很多其他的子句,包括requires-transitive、exports-to、open、opens、uses和provides。下面一一介绍这些子句。

(1) requires

requires子句可以在编译和运行时帮你设定你的模块对另一模块的依赖。譬如,模块com. iteratrlearning.application依赖于com.iteratrlearning.ui:

module com.iteratrlearning.application {          requires com.iteratrlearning.ui;}

执行这条子句的结果是模块com.iteratrlearning.application只能访问模块com. iteratrlearning.ui中声明为公有的类型。

(2) exports

exports子句可以将某些包声明为公有类型,提供给其他的模块使用。默认情况下,模块中所有的包都不导出。只能通过显式声明的方式导出包,让你对模块的封装性有了更严格的控制。下面这个例子中,com.iteratrlearning.ui.panels和com.iteratrlearning.ui. widgets都被导出了。(注意:exports接受的参数是包名,而requires接受的参数是模块名。虽然二者都采用了类似的命名模式,但仍有区别。)

module com.iteratrlearning.ui {          requires com.iteratrlearning.core;          exports com.iteratrlearning.ui.panels;          exports com.iteratrlearning.ui.widgets;}

(3) requires的传递

你可以声明一个模块能够使用另一个模块依赖的公有类型的包。譬如,你可以修改模块com.iteratrlearning.ui的声明,将requires子句变更为requires-transitive达到该效果,如下所示:

module com.iteratrlearning.ui {          requires transitive com.iteratrlearning.core;          exports com.iteratrlearning.ui.panels;          exports com.iteratrlearning.ui.widgets;}module com.iteratrlearning.application {          requires com.iteratrlearning.ui;}

这段声明的效果是模块com.iteratrlearning.application可以访问com.iteratr- learning.core导出的公有类型的包。当一个被依赖的模块(譬如这个例子中的com.iteratrlearning.ui)返回该模块自身依赖的模块(com.iteratrlearning.core)的类型时,传递性就非常有价值了。想象一下,如果需要在模块com.iteratrlearning. application中重复声明com.iteratrlearning.core的依赖,也是很烦人的事情。这个问题被transitive解决了。现在,依赖于com.iteratrlearning.ui的包自动地就能访问com.iteratrlearning.core模块。

(4) exports to

你对模块的可见性可以做进一步的控制,通过exports to结构,可以限制哪些用户能访问哪些导出的包。通过调整模块声明,你可以对14.8.2节中的例子做更细粒度的控制,只允许com.iteratrlearning.ui.widgets访问com.iteratrlearning.ui.widgetuser, 如下所示:

module com.iteratrlearning.ui {          requires com.iteratrlearning.core;           exports com.iteratrlearning.ui.panels;          exports com.iteratrlearning.ui.widgets to              com.iteratrlearning.ui.widgetuser;}

(5) open和opens

模块声明中使用open限定符能够让其他模块以反射的方式访问它所有的包。open限定符在模块的可见性方面没有特别的效果,唯一的作用就是允许对模块进行反射访问,如下所示:

open module com.iteratrlearning.ui { }

Java 9之前,你就能借助反射查看对象的私有状态。换句话说,没有什么是真正完全封装的。对象关系映射(object-relational mapping,ORM)工具,譬如Hibernate,就经常利用这种能力直接访问和修改对象的状态。默认情况下,Java 9不允许执行反射了。前面代码中的open子句提供了一种途径,允许在需要的时候进行反射。

你可以按照需要使用open子句对模块中的某个包执行反射,而不是对整个模块执行反射。此外,你还可以像exports-to限制导出模块的访问那样, 为open添加to限定符,限制哪些模块可以执行反射访问。

(6) uses和provides

如果你熟悉服务和ServiceLoader,接下来的内容可能就轻车熟路了。在Java模块系统中,你也可以使用provides子句创建服务供应方,使用users子句创建服务消费者。然而这个主题有点复杂,超出了本章的范畴。如果你对整合模块以及服务装载器感兴趣,建议你参考更广泛的学习资源,譬如本章前面提到的由Nicolai Parlog编写的The Java Module System。

9. 通过一个更复杂的例子了解更多

通过下面这个例子,你可以感受一下生产环境中的模块系统是怎样的,该例子摘自Oracle公司提供的Java文档。这个例子使用了本章中介绍的模块声明的大多数特性。采用这个例子并不是要吓唬你(其中大多数模块声明还是简单的exports和requires),只是让你了解一下模块丰富的特性。

module com.example.foo {  requires com.example.foo.http;requires com.example.foo.http; requires java.logging;  requires transitive com.example.foo.network; 	 exports com.example.foo.bar; exports com.example.foo.internal to com.example.foo.probe;  opens com.example.foo.quux; opens com.example.foo.internal to com.example.foo.network, com.example.foo.probe;  uses com.example.foo.spi.Intf; provides com.example.foo.spi.Intf with com.example.foo.Impl;}

本章讨论了新的Java模块系统诞生的原因并概要地介绍了它的主要特性。我们并没有介绍很多的特性,像服务装载器、附加模块描述符子句、辅助模块工作的工具,如jdeps和jlink都没有涉及。如果你是Java企业版的开发者,请注意将你的应用迁移到Java 9时,好几个与Java企业版相关的包默认都无法由模块化的Java 9虚拟机加载。譬如JAXP API类就属于Java EE API,它在Java SE 9默认的类路径中不存在。你需要显式地通过命令行开关--add-modules添加需要的模块,才能保证前后向的兼容性。譬如,要添加java.xml.bind,你就需要指定--add- modules java.xml.bind。

正如前文多次提到的那样,完整地介绍Java模块系统需要一本书,而不仅仅是这短短的一章。如果你希望更深入地理解模块系统的细节,建议你阅读由Nicolai Parlog编写的The Java Module System。

10. 小结

以下是本章中的关键概念。

□ 关注点隔离和信息隐藏是构造结构良好、易于维护与理解的软件的重要原则。

□ Java 9之前,你可以根据特定的需求,利用包、类以及接口对代码进行模块化,不过以上这些方式都缺乏足够的特性,无法进行有效的封装。

□ “类路径地狱”问题导致我们很难对应用的依赖性进行分析。

□ Java 9之前,JDK还是单体型的结构,导致很高的维护成本并限制了Java的演进。

□ Java 9引入了新的模块系统,它通过module-info.java文件命名模块,指定其依赖性(通过requires)以及导出的公共API(通过exports)。

□ 使用requires子句,你可以指定一个模块对其他模块的依赖。

□ 使用exports子句可以导出模块中的某些包,将其声明为公有类型,提供给其他模块使用。

□ 推荐使用互联网域名的逆序作为模块的命名方式。

□ 位于模块路径上且没有提供module-info文件的JAR文件会被Java 9作为自动模块处理。

□ 自动模块隐式地导出其全部包给其他模块使用。

□ Maven支持按照Java 9模块系统构建的应用。


——本文内容摘自《Java 实战(第2版)》


目录

第一部分 基础知识

第1章 Java 8、9、10以及11的变化  2


第2章 通过行为参数化传递代码  22


第3章 Lambda表达式  37


第二部分 使用流进行函数式数据处理

第4章 引入流  72


第5章 使用流  86


第6章 用流收集数据  118


第7章 并行数据处理与性能  151


第三部分 使用流和Lambda进行高效编程

第8章 Collection API的增强功能  176


第9章 重构、测试和调试  189


第10章 基于Lambda的领域特定语言  210


第四部分 无所不在的Java

第11章 用Optional取代null  242


第12章 新的日期和时间API  263


第13章 默认方法  278


第14章 Java模块系统  295


第五部分 提升Java的并发性

第15章 CompletableFuture及反应式编程背后的概念  316


第16章 CompletableFuture:组合式异步编程  344


第17章 反应式编程  370


第六部分 函数式编程以及Java未来的演进

第18章 函数式的思考  396


第19章 函数式编程的技巧  409


第20章 面向对象和函数式编程的混合:Java和Scala的比较  433


第21章 结论以及Java的未来  448


附录A 其他语言特性的更新  463

附录B 其他类库的更新  467

附录C 如何以并发方式在同一个流上执行多种操作  475

附录D Lambda表达式和JVM字节码  483

社区试读:

Java实战(第2版)-图书-图灵社区

购买连接:

京东网上商城

《Java实战 第2版》([英]拉乌尔?C加布里埃尔·乌尔玛 [意]马里奥·富斯科 [英]艾伦·米克罗夫特)【简介_书评_在线阅读】 - 当当图书

标签: #apachefelixss