java9学习之模块化 - CSDN
  • Java 9Java 平台引入了模块系统,这是一个非常重要的飞跃,标志着模块化 Java 软件开发的新纪元。当需要创建灵活且易于维护的代码时,模块化是一个关键的架构设计原则。本书给出了 Java 模块系统的明确概述,并...

    内容简介

    Java 9 向 Java 平台引入了模块系统,这是一个非常重要的飞跃,标志着模块化 Java 软件开发的新纪元。当需要创建灵活且易于维护的代码时,模块化是一个关键的架构设计原则。本书给出了 Java 模块系统的明确概述,并演示了如何通过创建模块化应用程序来帮助管理以及降低复杂性。

    作者引导我们了解了模块系统中的相关概念以及工具,介绍了可以将现有代码迁移到模块中的模式并以模块的方式构建新的应用程序。

    本书特色:

    • 了解 Java 平台自身如何实现模块化;
    • 学习模块化如何影响应用程序的设计、编译、打包以及开发;
    • 编写自己的模块;
    • 使用模式改进任意代码库的可维护性、灵活性以及重用性;
    • 学习如何使用服务来创建解耦模块;
    • 将现有代码迁移到模块,并学习如何使用并不是模块的现有库;
    • 创建优化的自定义运行时映像,从而改变装载模块化 Java 应用程序的方式。

    作者简介

    桑德 · 马克(Sander Mak),荷兰 Luminis 公司的一名研究员,开发了许多主要用于 JVM 上的模块化以及可扩展软件,但也会在需要的地方使用 TypeScript。他经常在各种会议上发言,并热衷于通过博客(http://branchandbound.net)和作为 Pluralsight 平台的讲师分享知识。

    保罗 · 巴克(Paul Bakker) Netflix 公司的一名高级软件工程师,在其 Edge Developer Experience 团队主要从事工具的开发,以提高公司内部开发人员的工作效率。曾与他人合作编著了 Modular Cloud Apps with OSGi(由O''Reilly 出版公司出版)一书。Paul 经常在与模块化、容器技术相关主题的会议上发言。

    本书内容

    译者序

    JDK 9是 Java 开发工具包的第9个主要版本,于2017年7月下旬发布,它带来了许多令人兴奋的新功能。Java 9定义了一套全新的模块系统。当代码库越来越大,创建盘根错节的“意大利面条式代码”的概率呈指数级增长,这时候就得面对两个基础问题。首先,很难真正地对代码进行封装,而系统对不同部分(也就是 JAR 文件)之间的依赖关系并没有明确的概念。每一个公共类都可以被类路径之下任何其他公共类所访问,这样就会导致无意中使用了并不想被公开访问的 API。其次,类路径本身也存在问题:你怎么知晓所有需要的 JAR 都已经有了,或者是不是会有重复的项呢?模块系统把这两个问题都解决了。

    模块化的 JAR 文件都包含一个额外的模块描述符。在这个模块描述符中,对其他模块的依赖是通过 requires 来表示的。另外,exports 语句控制着哪些包是可以被其他模块访问的。所有不被导出的包默认都封装在模块里。

    本书共分为三部分,第一部分包括6章。第1章主要介绍了什么是模块化以及 Java 9模块的主要特点。第2章学习了如何定义模块,以及使用哪些概念管理模块之间的交互。第3章在第2章的基础上通过构建自己的模块进一步学习相关模块概念。第4章讨论了可以解耦模块的服务。第5章和第6章探讨了模块化模式,以及如何以最大限度地提高可维护性和可扩展性的方式使用模块。

    第二部分包括4章。第7章和第8章重点介绍了如何将现有的代码迁移到模块中。第9章通过迁移案例详细讨论了如何实现迁移。如果你是一名库的创建者或者维护者,那么第10章将对你有所帮助,其介绍了如何向库添加模块支持。

    第三部分也包括4章,主要介绍了一些模块化开发工具。第11章学习了主要的 IDE 以及构建工具。第12章介绍了如何对模块进行测试。第13章和第14章主要介绍了自定义运行时映像以及对模块化未来的展望。

    本书图文并茂、技术新、实用性强,以大量的实例对 Java 9模块系统做了详细的解释,是学习 Java 9的读者不可缺少的实用参考书籍。本书可作为 Java 编程人员的参考手册,适合计算机技术人员使用。此外,书中还提供了相关参考资料,如果在阅读过程中遇到不明白的方法或属性,可以参阅相关内容。

    参与本书翻译的人有王净、田洪、范园芳、范桢、胡训强、晏峰、余佳隽、张洁、何远燕、任方燕。最终由王净负责统稿。在此,要感谢我们的家人,他们总是无怨无悔地支持我们的一切工作。

    在翻译过程中,我们尽量保持原书的特色,并对书中出现的术语和难词难句进行了仔细推敲和研究。但毕竟有少量技术是译者在自己的研究领域中不曾遇到过的,所以疏漏和争议之处在所难免,望广大读者提出宝贵意见。

    最后,希望广大读者能多花些时间细细品味这本凝聚作者和译者大量心血的书籍,为将来的职业生涯奠定良好的基础。

    王净

    2018年3月于广州

    什么是 Java 中的模块化?对于一些人来说,模块化是一个开发原则,即对接口进行编程并隐藏实现的细节,这就是所谓的封装学派(school of encapsulation)。对于另外一些人来说,模块化是指依赖类加载器来提供动态执行环境,这就是所谓的隔离学派(school of isolation)。还有一些人认为模块化指的是工件、存储库以及相关工具,这就是所谓的配置学派(school of configuration)。虽然单独来看,这些观点都是正确的,但它们都太片面,感觉更像是一个不太清晰的“大故事”的几个片段。如果开发人员知道其部分代码仅供内部使用,那么他们为什么不能像隐藏类或字段一样容易地隐藏包呢?如果代码只能在依赖项存在的情况下编译和运行,那么这些依赖项为什么不能顺畅地从编译过程流向打包过程,再到安装过程,最后到执行过程呢?如果工具只有在提供了原始自描述工件时才能起作用,那么如何重用那些只是普通 JAR 文件的旧版本库呢?

    Java 9将模块作为 Java 平台的高级功能,从而很自然地引入了模块化概念。模块是一组用于重用的包,这个简单的概念对代码开发、部署以及运行的方式产生了非常深刻的影响。一旦将包放置到模块中,Java 中用来促进和控制“重用”的长期存在的机制(接口、访问控制、JAR 文件、类加载器以及动态链接)就能更好地工作。

    首先,模块以其他机制无法实现的方式阐明了程序的结构。许多开发人员会惊讶地发现他们的代码结构并没有想象得那么好。例如,由多个 JAR 文件组成的代码库可以实现不同 JAR 文件中类之间的循环,但在不同模块中的类之间的循环却是禁止的。实现代码库模块化的动机之一是一旦实现了模块化,就可以避免出现因为循环依赖所产生的“泥球”(ball of mud)[1]。此外,使用模块进行开发还可以实现通过服务进行编程,从而减少耦合并进一步提高抽象性。

    其次,模块产生了其他机制无法实现的代码责任感。从模块中导出包的开发人员实际上对 API 的稳定性做出了承诺,甚至模块本身的名称也是 API 的一部分。如果开发人员将太多的功能捆绑到单个模块中,那么就会导致该模块牵扯到大量与任何单一任务无关的依赖项;任何重用该模块的人都会意识到其杂乱无序的性质,即使模块的内部是隐藏的。使用模块进行开发可以促使每个开发人员思考其代码的稳定性和内聚性。

    大多数人对桌布戏法都非常熟悉,即将桌布从桌子上迅速拿走,同时不能打翻盘子和杯子。对于那些使用 Java 9的人来说,设计一个可以嵌入 Java 虚拟机(Java 虚拟机由自20世纪90年代以来所开发的数以百万计的类所控制)的模块系统感觉就像是反向表演桌布戏法。事实证明,模块化 JDK 导致了戏法的失败,因为一些知名的库为了自身的发展而不愿意将模块系统应用于 JDK 模块所带来的封装。Java 9设计中的这种矛盾很难在学术上得到解决。最终,来自社区的长期反馈促使模块系统为开发人员提供了各种各样的“杠杆”和“调节盘”,使得模块化平台代码可以享受真正强大的封装,而模块化应用程序代码可以享受“足够强大”的封装。随着时间的推移,我们认为在模块化 JDK 方面进行的大胆选择将会使得代码更加可靠。

    只有当一个模块系统适用于所有人时,该系统才是最好的。今天创建模块的开发人员越多,明天就会有更多的开发人员创建模块。但是那些尚未创建自己模块的开发人员又该怎么办呢?毫不夸张地说,Java 9会像关注模块内的代码一样关注模块外的代码。代码库的作者是唯一应该对代码库进行模块化的开发人员,在完成模块化之前,模块系统必须为模块中的代码提供一种方法来“接触”模块之外的代码,而这也导致了自动模块的设计,本书将会详细介绍这部分内容。

    Sander 和 Paul 都是 Java 方面的专家,同时也是 Java 9生态系统可信任的指导者。他们身处 Java 9开发的最前沿,是迁移流行开源库的先驱。本书面向那些对 Java 中模块化的核心原则和最佳实践感兴趣的人的,包括希望创建可维护组件的应用程序开发人员,寻求关于迁移和反射建议的库开发人员,以及希望利用模块系统高级功能的框架开发人员。我希望本书可以帮助你创建出程序结构经得起时间考验的 Java 程序。

    Alex Buckley

    Oracle Java 平台组

    圣克拉拉,2017年7月


    [1] 泥球是指一个随意化的杂乱的结构化系统,只是代码的堆砌和拼凑,往往会导致很多错误或者缺陷。—译者注

    前言

    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)的许可。本书的作者将漫画改为横向和灰色。

    第1章 模块化概述
    第2章 模块和模块化 JDK
    第3章 使用模块
    第4章 服务
    第5章 模块化模式(一)
    第5章 模块化模式(二)
    第6章 高级模块化模式
    第7章 没有模块的迁移
    第8章 迁移到模块
    第9章 迁移案例研究:Spring 和 Hibernate
    第10章 库迁移
    第11章 构建工具和 IDE
    第12章 测试模块
    第13章 使用自定义运行时映像进行缩减
    第14章 模块化的未来

    阅读全文: http://gitbook.cn/gitchat/geekbook/5c189a881e59245d4d2aac4f

    展开全文
  • 模块化Java9中是炒鸡炒鸡重要的!今后,模块将是应用程序的基础构建单位。无论是使用动态语言或静态语言编写,模块都是应用程序的一个重要组成部分,且模块遵循单一功能原则。你可以将任意方法称为模块,正如函数...

    Well, Java 9 is available so let’s make modules!

    模块化在Java9中是炒鸡炒鸡重要的!今后,模块将是应用程序的基础构建单位。无论是使用动态语言或静态语言编写,模块都是应用程序的一个重要组成部分,且模块遵循单一功能原则。你可以将任意方法称为模块,正如函数式编程也是基于模块一样。好了,总之,你get到这个点之后就记住:以后模块很重要很重要很重要 The important thing is need to be said 3 times!

    这里写图片描述

    步骤:

    • 步骤1:下载Java 9并安装
    • 步骤2:创建一个模块java文件
    • 步骤3:模块文件路径
    • 步骤4:编写一个模块
    • 步骤5:向我们的模块添加代码
    • 步骤6:编译我们的模块
    • 步骤7:运行我们的模块

    1、下载Java 9并安装

    下载地址:http://www.oracle.com/technetwork/java/javase/downloads/jdk9-downloads-3848520.html

    下载后,只需点击它即可进行安装(如果您在MacOS上),并确认已安装:

    tomerb@tomerb-mac.local:~$ java --version
    java 9-ea
    Java(TM) SE Runtime Environment (build 9-ea+164)
    Java HotSpot(TM) 64-Bit Server VM (build 9-ea+164, mixed mode)
    tomerb@tomerb-mac.local:~$ cd ~/tmp
    tomerb@tomerb-mac.local:~$ mkdir -p ~/tmp/java9-modules
    tomerb@tomerb-mac.local:~$ cd ~/tmp/java9-modules

    2、创建一个模块java文件

    在Java 9中,为了定义模块,需要为java文件设定一个特别的名字,建议为: module-info.java

    3、模块文件路径

    module-info.java 应该放在哪里呢?按照惯例,应该把它放在和模块名相同的目录下。

    如果你的模块名称是
    
    com.me.mymodule
    
    那么你的模块  module-info.java 应该放在:
    
    src/com.me.mymodule
    
    这使您的  module-info.java 文件放在:
    
    src/com.me.mymodule/module-info.java 路径。
    
    得到它了? <module-path> == <module name> 

    4、编写一个模块

    现在我们知道我们的模块 filename 和我们的模块 filepath,我们来编写一个具有这个命名和文件夹约定的模块:

    tomerb@tomerb-mac.local:~/tmp/java9-modules$ mkdir -p src/com.me.mymodule
    tomerb@tomerb-mac.local:~/tmp/java9-modules$ vi src/com.me.mymodule/module-info.java
    module com.me.mymodule { }

    至此,我们就完成了一个模块的编写!

    5、向我们的模块添加代码

    在这一步中,我们将向我们的模块添加一些代码!在同一目录下创建一个新的java文件来作为我们的模块:

    $ mkdir -p src/com.me.mymodule/com/me/mymodule
    $ vi src/com.me.mymodule/com/me/mymodule/Main.java

    注意我们代码中的目录名称。为什么这样做呢? 我们首先输入我们模块所在的路径,然后我们为源代码创建完整的包名称。在这种情况下, /com/me/mymodule/com.me.mymodule 的上层。只是我们的源文件属于我们的模块,并且模块已经在Java 9惯例的标准模块目录中。

    所以,我们就在Main.java编写hello world:

    package com.me.mymodule;
    public class Main {
        public static void main(String[] args) {
            System.out.println("Hello World from Module! :)"); // nothing special here, standard java < 9 class.
        }
    }

    6、编译我们的模块

    首先制作 mods 目录(我们将进一步传递给: java --module-path):

    $ mkdir -p mods/com.me.mymodule
    $ javac -d mods/com.me.mymodule \
              src/com.me.mymodule/module-info.java \
              src/com.me.mymodule/com/me/mymodule/Main.java

    轻动你高贵的手指,让编译器再跑一会儿!

    7、运行我们的模块

    $ java --module-path mods -m com.me.mymodule/com.me.mymodule.Main
    Hello World from Module! :)

    总结

    在这部分中,我们下载了Java 9,创建了一个模块,向其中添加了一个源文件,并运行它。我们看到在创建源代码时创建模块路径和类似的东西时,需要遵循一个命名约定。

    原文:https://dzone.com/articles/java-9-tutorial-step-by-step-from-zero-to-modules

    展开全文
  • 通过Java平台模块化系统(JPMS),开发者可以定义他们的应用程序模块,决定其他模块如何调用他们的模块,以及他们的模块如何依赖其他模块。 对于已经使用了其他模块系统(如Maven或Gradle)的应用程序来说,还是有...

    要点

    • 通过模块化的方式开发应用程序,实现更好的设计,如关注点分离和封装性。
    • 通过Java平台模块化系统(JPMS),开发者可以定义他们的应用程序模块,决定其他模块如何调用他们的模块,以及他们的模块如何依赖其他模块。
    • 对于已经使用了其他模块系统(如Maven或Gradle)的应用程序来说,还是有可能再加入JPMS。
    • JDK为开发者提供了一些工具,用于将现有的代码迁移到JPMS。
    • 应用程序代码仍然可以依赖Java 9之前的类库,这些类库的jar包被看成是一种特别的“自动化”模块,从而简化了向Java 9迁移的工作。

    这篇文章提供了一个学习案例,演示一个真实的应用程序需要做出哪些变更才能迁移到JPMS。当然,要使用Java 9不一定要做这些事情,不过对于Java开发者来说,了解模块化系统(通过被叫作Jigsaw)无疑是一个非常重要的技能。

    我将会演示如何一步步地使用新的Java模块化系统来重构一个基于Java 8的应用程序。

    下载Java 9

    首先要下载和安装最新版本的JDK 9,目前只有抢先预览版(这里使用的是9-ea+176版本)。在全面了解Java 9之前,你可能不希望把它作为系统的默认Java版本。所以,你可以不改变原先的$JAVA_HOME环境变量,而是创建一个新的变量$JAVA9_HOME,并把它指向新安装的JDK目录。我将在这篇文章里使用这个新变量。

    关于使用Java 9要做的其他一些步骤,可以参看其他现成的教程。我们主要还是讨论模块化组件,不过你也可以参考Oracle的迁移指南。

    模块化

    关于Java 9,你会经常听到人们谈论Jigsaw项目,也就是Java的新模块化系统。关于Jigsaw已经有很多相关教程,而这篇文章将介绍如何使用JPMS对已有代码进行迁移。

    要使用Java 9,并不一定要在代码里添加模块化,这一点让很多开发者都感到惊讶。开发者在使用Java 9时最为关注的一点或许是内部API的封装性,虽然这个会影响到开发者,但这并不意味着要使用Java 9就一定要完全拥抱模块化。

    要利用好JPMS,有很多工具可以帮到你,比如jdeps依赖分析器、Java编译器和你所使用的IDE。

    我不会在这里讲解如何将应用程序拆解成模块,如果一开始没有做好模块化规划,后续就会变得很困难(JDK的模块化拆解就花了好几年的时间)。相反,我假设你的应用程序已经是按照小型的部件组织在一起的,它们可能是Maven模块或者Gradle子项目,又或者是IDE里的子项目或模块。

    你会发现,在很多教程里,包括Jigsaw的入门指南,都会假设一个如下所示的项目结构。

    这里写图片描述
    图1

    项目里有一个单独的src目录和一个单独的test目录,其他所有模块都是这两个目录的子目录。这个与Maven或Gradle的结构不太一样,它们的每个模块都有自己的src目录和test目录。不过好在你不一定要重新组织整个应用程序的代码结构(也不需要让你的构建工具重新去理解这种结构),你可以继续使用Maven或Gradle的结构,只要你知道在不同的教程里可能使用了不同的结构。关键是你要知道应用程序的根目录是哪一个——在Maven或Gradle里,根目录就是src目录和test目录。

    这里写图片描述
    图2

    你要做的第一件事情是在模块的根目录放置一个module-info.java文件,用来定义模块的名字。你可以手动创建这个文件,也可以让IDE帮你创建这个文件。图3展示了我的模块的module-info.java文件:

    这里写图片描述
    图3

    现在在IDE里编译项目,或者在src目录通过命令行来编译模块:

    > "%JAVA9_HOME%"\bin\javac -d ..\mods\service module-info.java com\mechanitis\demo\sense\service\*.java 
    com\mechanitis\demo\sense\service\config\*.java

    这个时候你会发现有很多编译错误(如图4所示)。

    这里写图片描述
    图4

    总共有27个错误,或许你会感到很惊讶,之前这个项目完全可以编译并正常运行,但在添加了一个module-info.java文件之后就无法编译了。问题在于,我们现在要显式地指定我们的模块所依赖的其他模块。这些模块包括JDK的模块、我们自己创建的其他模块,或者来自外部依赖的模块(在这里我们需要自动模块)。

    jdeps依赖项分析器可以帮助我们确定需要在module-info.java里声明哪些模块。为了让程序运行起来,你需要一些东西:

    一个包含模块代码的jar包,或者一个包含class文件的目录。要注意,在上一步编译之后根本得不到class文件,你需要先移除module-info.java文件后重新编译才能得到需要的class文件。
    模块代码的类路径。如果你习惯了在IDE里运行程序,并使用了Maven或Gradle来管理依赖,可能就很难找到或设置类路径。在IntelliJ IDEA里,你可以在运行窗口中看到类路径。如图5所示,我把滚动条滚动到适当的位置,然后把蓝色字体的内容拷贝出来。

    这里写图片描述
    图5

    现在我们可以运行jdeps,并使用Java 9的一些标记:

    > "%JAVA9_HOME%"\bin\jdeps --class-path %SERVICE_MODULE_CLASSPATH% out\production\com.mechanitis.demo.sense.service

    最后一个参数是包含了class文件的目录。在运行这个命令的时候,我们会得到如下的输出。

    split package: javax.annotation [jrt:/java.xml.ws.annotation, C:\.m2\...\javax.annotation-api-1.2.jar]
    
    com.mechanitis.sense.service -> java.base
    com.mechanitis.sense.service -> java.logging
    com.mechanitis.sense.service -> C:\.m2\...\javax-websocket-server-impl-9.4.6.jar
    com.mechanitis.sense.service -> C:\.m2\...\javax.websocket-api-1.0.jar
    com.mechanitis.sense.service -> C:\.m2\...\jetty-server-9.4.6.jar
    com.mechanitis.sense.service -> C:\.m2\...\jetty-servlet-9.4.6.jar
       com.mechanitis.sense.service    -> com.mechanitis.sense.service.config  com.mechanitis.sense.service
       com.mechanitis.sense.service    -> java.io                              java.base
       com.mechanitis.sense.service    -> java.lang                            java.base
       com.mechanitis.sense.service    -> java.lang.invoke                     java.base
       com.mechanitis.sense.service    -> java.net                             java.base
       com.mechanitis.sense.service    -> java.nio.file                        java.base
       com.mechanitis.sense.service    -> java.util                            java.base
       com.mechanitis.sense.service    -> java.util.concurrent                 java.base
       com.mechanitis.sense.service    -> java.util.concurrent.atomic          java.base
       com.mechanitis.sense.service    -> java.util.function                   java.base
       com.mechanitis.sense.service    -> java.util.logging                    java.logging
       com.mechanitis.sense.service    -> java.util.stream                     java.base
       com.mechanitis.sense.service    -> javax.websocket                      javax.websocket-api-1.0.jar
       com.mechanitis.sense.service    -> javax.websocket.server               javax.websocket-api-1.0.jar
       com.mechanitis.sense.service    -> org.eclipse.jetty.server             jetty-server-9.4.6.jar
       com.mechanitis.sense.service    -> org.eclipse.jetty.servlet            jetty-servlet-9.4.6.jar
       com.mechanitis.sense.service    -> org.eclipse.jetty.websocket.server        javax-websocket-server-impl-9.4.6.jar
       com.mechanitis.sense.service    -> org.eclipse.jetty.websocket.server.deploy javax-websocket-server-impl-9.4.6.jar
       com.mechanitis.sense.service.config           -> java.lang              java.base
       com.mechanitis.sense.service.config           -> java.lang.invoke       java.base
       com.mechanitis.sense.service.config           -> javax.websocket        javax.websocket-api-1.0.jar
       com.mechanitis.sense.service.config           -> javax.websocket.server javax.websocket-api-1.0.jar

    这些是分裂包(split pacakge)的警告信息,说明相同的包名出现在两个不同的模块或jar文件里。在我们的例子里,相同的包名同时出现在了java.xml.ws.annotation(来自JDK)和javax.annotation-api.jar里。这些信息还包含了我的模块所使用的所有包名,以及这些包所在的模块或jar文件。我可以使用这些信息来创建module-info.java文件:

    module com.mechanitis.demo.sense.service {
       requires java.logging;
       requires javax.websocket.api;
       requires jetty.server;
       requires jetty.servlet;
       requires javax.websocket.server.impl;
    }

    java.base包含了几乎所有的JDK基本类库,不过我不需要显示地声明它,因为它是默认包含的。我甚至不需要知道外部依赖项的自动模块名称。

    自动模块

    Java 9和JPMS考虑到大部分的代码在一开始并不会使用JPMS(例如并没有通过module-info.java来定义依赖和权限从而实现完全的模块化)。为了解决这个问题并简化迁移工作,你的模块化代码仍然可以使用jar包依赖(这些jar包不是真正的模块)。这些jar包被称为自动模块,它们内部的包名可以被自由访问,只要它们处在类路径里,你就可以像以前那样随意访问它们。对于开发者来说,我们唯一要做的就是找出它们的名字。默认情况下,它们的名字一般就是去掉了版本号的jar包文件名。例如,jetty-server-9.4.1.v20170120.jar对应的自动模块名就是jett.server(使用点号代替了破折号)。

    注意:最新版本的Java 9允许开发者通过jar包的manifest属性“Automatic-Module-Name”指定自动模块的名字,所以你可以通过检查jar包来找出模块名。

    使用我们的新模块

    现在我的代码可以通过编译,接下来让我们来迁移另一个模块。先创建一个空的module-info.java文件,图6显示了新的编译错误。

    这里写图片描述
    图6

    要修复这些错误非常简单,只要更新module-info.java文件就可以了:

    module com.mechanitis.demo.sense.service {
       requires java.logging;
       requires javax.websocket.api;
       requires jetty.server;
       requires jetty.servlet;
       requires javax.websocket.server.impl;
    
       exports com.mechanitis.demo.sense.service;
    }

    重新编译后生成另一个错误:

    Error:(3, 33) java: package com.mechanitis.demo.sense.service is not visible
      (package com.mechanitis.demo.sense.service is declared in module com.mechanitis.demo.sense.service, but module com.mechanitis.demo.sense.user does not read it)
    这说明要为user模块声明service模块:
    
    module com.mechanitis.demo.sense.user {
       requires com.mechanitis.demo.sense.service;
    }

    然后就可以正常编译模块,并成功运行user服务。

    结论

    我们演示了如何重构一个已有的应用程序,让它用上JPMS。将应用程序拆分成模块是有好处的,将应用程序迁移到JMPS也是一件很有意义的事情,不过在这样做之前要先确定这样确实能给你带来好处。

    展开全文
  • 有过编程经验的人都不应该对模块化陌生, 无论是项目代码的组织、还是应用的拆分、架构的设计都渗透着模块化的思想,如今jdk9 不但本身已经模块化,且对我们创建模块化的应用程序提供了“本地原生”支持。模块化的...

    Jdk9模块化实战入门

    本章我们来到了Jdk9新增的最重要的一块内容——模块化。有过编程经验的人都不应该对模块化陌生, 无论是项目代码的组织、还是应用的拆分、架构的设计都渗透着模块化的思想,如今jdk9 不但本身已经模块化,且对我们创建模块化的应用程序提供了“本地原生”支持。模块化的程序提供了更高级的封装性与服务化特性。本章我们将学习:

    • 什么是模块化
    • jdk9 的模块化特性
    • 如何构建模块化程序
    • 面向服务的模块化设计

    一、模块化思想进阶

    模块化的目标

    比如说一个电商系统根据业务可以拆解为登录、购物车、商品详情、订单、秒杀等等子系统, 比如说一个app端商品列表页可以拆分成header barfooter barcategory tab 、搜索框、商品列表等一系列UI 组件。再比如我们总是把一些通用的逻辑抽取到一块, 比如我们的工具类。大到应用系统设计,小到功能编码。从现在很火的前端vuereact 编程模型, 到后端兴起的Spring-cloud 微服务架构等等,全都在深入贯彻着模块化思想: 把一个大的系统拆分成细粒度的组织单元,进而降低系统复杂度、减少耦合性,增加复用性,提高团队协作能力,所以模块化的目标:

    • 拆分思想
      当我们生活中遇到一个很大的困难时, 我们肯定是把它分解成一个个的小问题, 分而治之,各个击破。同理,模块化的原则驱动我们去把一个大的代码逻辑拆分成一个个小的功能函数, 如果能有机的组织好这些小的功能单元, 那对外同样可以提供更大服务能力,并且每个功能单元的可复用性大大增加、复杂度大大减少,有利于专门的开发人员去维护!

    • 封装性&服务化
      在构建模块时,所谓封装, 意味着对外要隐藏一些属性以及实现细节, 模块之间通过接口通信, 作为消费者的模块只需要关注服务提供者模块暴露的api, 而隐藏掉接口的实现细节,进而阻止意外的模块间耦合(如果另一模块不小心使用了一个模块应该封装的东西),减少了访问模块的复杂性, 提高了安全性。

    jdk9之前

    问题一

    jdk9之前, 如果我们做这样一个事情: 设计一个通用数据加密工具jar 来让开发团队各个小组使用,我们可能会设计一个通用的加密接口来对外让大家使用,而不需要去关心加密方式。

    └─com.dockerx
       └─api
           └─internal
               └─ DataSecurityServiceImpl
           └─ DataSecurityService
       public static String encrypt(String info, KeyType type) {
            return DataSecurityServiceImpl.sysInnerEncryptBySHA(info, type);
        }

    DataSecurityService 里包含一个静态加密方法,加密过程委托给internal 包下的实现类,现在你可能将代码打成jar 包, 让大家去引用你的jar,然后在代码中这样去调用:DataSecurityService.encrypt("password", KeyType.RSA) , 一切看起来很美好。一个月后你发现一个安全性更高的加密算法, 并决定更新曾经的jar, 心想只需要删掉之前的实现类,换用新的实现不就行了?而客户端对加密接口的调用方式不需要做任何改动!

       public static String encrypt(String info, KeyType type) {
            return RSADataSecurityServiceImpl.sysInnerEncryptByRSA(info, type);
        }

    好了, 改动完毕,升级完jar 版本,大家都引用了最新的加密jar 包,结果各个项目中大面积代码报错!因为并不是所有人都像期望的那样使用了DataSecurityService.encrypt("password",KeyType.PASSWD) ,他们是直接使用具体实现类做的加密(DataSecurityServiceImpl.sysInnerEncryptBySHA(info, type))! 虽然是在internal 包下, 但仍然阻止不了程序员们探索的热情, 都怪实现类是public, 如果把实现类 改成protect呢? 那加密接口就无法访问实现类了!看来实现类只能是public 的, 只能赤裸裸的暴露给所有不该使用的人使用?

    问题二

    //重写
    我们知道classPath 对于Java 应用程序的意义,无论是在累的编译时还是运行时, 对类的加载都需要去classPath 遍历查找。在运行时一个Java 应用程序就JVM 来说,本质上就是来自各个包中的一堆在classPath 中平铺的class 文件集合而已。即使所谓的jarJVM 也是透明的。
    逻辑上就像这样:

    Java.lang.Integer; Java.lang.String; java.util.Map;javax.annotation.Resource;.........

    我们知道JVM 运行时所需的类库rt.jar 在经过二十多年的丰富以后,已经包含了两万多个类,如果算上我们大型应用程序所依赖的其它类, 全都毫无结构化的平铺在我们的classPath 中,维护起来越来越像噩梦一样,例如重复依赖某个类,而两者版本不一致,反映在classPath 中,那就看哪一个版本的类先被找到, 进而造成运行时异常的隐患,抑或在
    /*需要补充/

    jdk9 模块化

    尽管我们在应用层面已经随处可见模块化设计的理念, 但是直到jdk9 发布之后, 才有了“编程语言”级别的构建模块化应用程序的支持, 开发者们现在可以利用jdk9 对模块化的原生支持,构建模块化应用程序。就连java runtime environment(jre)Java Development Kit (JDK) 也已被重写为模块化,我们可以在控制台使用java --list-modules 来列出Java 平台提供的可用模块:

    C:\Users\cbam>java --list-moudles
    java.activation@9.0.1
    java.base@9.0.1
    java.compiler@9.0.1
    java.corba@9.0.1
    java.datatransfer@9.0.1
    java.desktop@9.0.1
    java.instrument@9.0.1
    java.jnlp@9.0.1
    java.logging@9.0.1
    java.management@9.0.1
    java.management.rmi@9.0.1
    ..............................
    ..............................

    /*需要补充/

    二、构建模块化实例

    创建单个模块

    创建多个模块

    有了单模块创建的基础, 我们来尝试把第一节加密jar 以模块化的方式重写, 看看能否其存在的问题。按如下目录结创建两个模块, 加密模块:com.dockerx.encrypt,客户端模块: com.dockerx.cli ,客户端模块依赖加密模块来调用加密服务。

    src
    └─com.dockerx.encrypt
        └─ module-info.java
        └─ security
               └─api
                   └─ DataSecurityService.java
               └─internal
                   └─ DataSecurityServiceImpl.java
    
    └─com.dockerx.cli
        └─ module-info.java
        └─ security
             └─ cli
                  └─ Main.java

    com.dockerx.encrypt 模块的module-info:

    module com.dockerx.encrypt {
        exports security.api;
    }
    

    com.dockerx.cli 模块的module-info:

    module com.dockerx.cli {
        requires com.dockerx.encrypt;
    }

    exports 意味着导出该模块下的某个包,意味着其它模块可以使用,这个包下的内容。requires 意味着此模块依赖com.dockerx.encrypt 模块。
    Main 类中代码:

    import security.api.DataSecurityService;
    import security.internal.DataSecurityServiceImpl;
    
    public class Main {
        public static void main(String[] args) {
     System.out.println(DataSecurityService.encrypt("Hello"));
        }
    }

    使用以下指令编译通过:

    javac --module-source-path src -d out -m com.dockerx.encrypt,com.dockerx.cli
    

    我们假设有某个“不老实” 的程序员故意要使用实现类来加密, 我们在main 方法中做一下尝试

        System.out.println( DataSecurityServiceImpl.sysInnerEncryptBySHA("Hello"));

    再次编译发现很不幸!现在我们无法访问具体实现了!明明我们的实现类是public 的!看来只能老老实实根据提供的接口来加密了。。。

    D:\project\Demo\jdk9\src\main\java>javac --module-source-path src2 -d out -
    m com.dockerx.encrypt,com.dockerx.cli
    src2\com.dockerx.cli\security\cli\Main.java:4: 错误: 程序包security.internal不存
    在
    import security.internal.DataSecurityServiceImpl;
                            ^
    src2\com.dockerx.cli\security\cli\Main.java:10: 错误: 找不到符号
            System.out.println( DataSecurityServiceImpl.sysInnerEncryptBySHA("Hello"
    ));
                                ^
      符号:   变量 DataSecurityServiceImpl
      位置: 类 Main
    2 个错误

    我们看到模块化的封装特性阻止了外部模块对此模块某些包下的某些类型访问能力, 即使某些类型是public 的,除非在module-info.java 里显示导出,否则就是不可见的
    我们再来看另外一个问题, jdk9 之前,我们编译好的class 运行的时候有时会出现NoClassDefFoundError 运行时异常。现在我们尝试把我们编译好的com.dockerx.encrypt 模块目录删掉,然后再次运行, 发现抛出了如下异常

    Error occurred during initialization of boot layer
    java.lang.module.FindException: Module com.dockerx.encrypt not found, required b
    y com.dockerx.cli

    异常显示com.dockerx.encrypt 模块被依赖, 却未找到,这并不意外, 但是我们最应该关注的是异常并不是发生在Java 运行时找不到某个类!意味着在虚拟机初始化的时候就对所有module-info 所构建的依赖图做了检查。(就好比我们这本书包含第4章节,但现在第四章节在目录里面消失了,当然要发生异常)
    问题到这里似乎都解决了? 如果哪天我们要扩展一些新的加密算法实现, 我们肯定要重新修改这个模块化的jar , 在internal 包里新增一个实现, 然后需要修改api 包中的DataSecurityService 的加密实现:

      public static String encrypt(String info, KeyType type) {
            switch (type)
                case KeyType.RSA:
                     return DataSecurityServiceImpl.sysInnerEncryptByRSA(info, type);
                 break;
                case KeyType.SHA:
                     return DataSecurityServiceImpl.sysInnerEncryptBySHA(info, type);
                 break;
                 。。。。。。。
        }

    别忘了还要在使用加密模块的客户端模块的module-info.javarequires 我们新的实现!看来真是一团糟。

    虽然模块化的封装性对客户端屏蔽了实现细节,但从扩展性上,我们发现对外加密的api 和加密算法实现, 不仅物理上耦合在一个包里, 就连代码逻辑都有着千丝万缕的联系。api 对外应该是一种稳定的存在, 如果在修改实现的时候还要改动api 的逻辑,系统稳定性将受到极大的威胁!“对扩展开放, 对修改关闭”的开闭原则激励我们去思考如何更优雅的扩展一种实现,而不需要做任何多余的修改。同时客户端要想使用加密功能, 还得知道存在哪些可用的实现,“最少知识原则”激励我们去思考如何才能使api 模块完全不需要知道实现类的存在与否甚至实现细节。 api 与实现类之间,实现类与实现类之间最好应该尽可能少的通信, 因为越解耦的系统,将来出错的机率越低!可维护性、可扩展性越强!带着这个问题我们在下一节通过持续优化来学习如何使模块间完全解耦的服务化特性~

    Tips:
    public is not public ! 如果读者的应用程序是基于Spring 的,可能直接用DI 来做这个工具类

    三、面向服务的模块化设计

    本章我们将共同学习jdk9 中模块间的“服务化” 支持,上一章中我们尝试抽取出通用加密模块来让各个客户端模块使用, 本身就是一种服务支持,只不过模块间通过一种直接相互依赖的方式,这种方式, 我们已经在上一章末尾讨论了存在的问题。现在我们继续重构上一章的例子一步步尝试着把模块间的依赖变成松耦合。

    简单重构

    首先,要表达一种服务,使服务具有扩展性,我们自然而然的想到定义一个接口,然后让不同的实现类实现这个接口, 虽然用接口来表达不是必须的,但却是一种极佳的方式, 那么我们把我们之前的类DataSecurityService 声明成接口类型,所有internal 包下的加密类现在都需要实现该接口。其次,为了使我们的接口与具体实现松耦合,更弹性的去扩展一种实现,我们现在把接口以及接口的所有实现都分别模块化,加上客户端,我们现在有四个模块要创建,就像这样:
    这里写图片描述
    让我们来分析一下上面的改造,现在我们可以轻松的扩展一种新的加密实现类,只要实现DataSecurityService 接口就好了嘛, 这时无须改动接口模块,既然我们的服务约定(接口定义)稳定,就确保了客户端的服务调用的稳定性。同时对于我们新的加密实现单独作为一个模块,对其它已稳定“跑着”的实现模块不会产生任何影响!看来我们的这次重构 带来的好处不少。好,我们来写一写DataSecurityClient 客户端模块的代码:

    DataSecurityService encryptService = new RsaEncryptImpl();
    encryptService.encrypt("identityCode");

    看到new, 是否回顾起来上章末尾遗留的另一个问题?客户端还是要拥有足够的“知识”去知道到底有哪些实现模块!毕竟客户端只拿着接口至少要找个具体的实现类的实例去进行加密吧。我们新扩展了一个加密实现模块还要记得去告诉好多好多的客户端:“哦~ 有新实现了!快添加新依赖~”,并且每次扩展都需要在module-infoexports 新的实现,甚至一旦服务实现类修改,客户端代码可能也要修改。所以说,现在又来问题了:客户端强耦合具体实现模块。反映在客户端module-info.java 里,会是这样
    //TODO 图上下次会标上 模块名称 文章内容就可以 直接称呼其模块名

    module com.dockerx.client {
       requires com.dockerx.encrypt.api;
       requires com.dockerx.encrypt.HmacImpl;
       requires com.dockerx.encrypt.RsaImpl;
    }

    意味着每次有新实现,客户端都要requires 一下。那么我们现在把目标转向如何如何使com.dockerx.client 模块和各种具体实现模块松耦合?

    工厂模式引入

    在面向接口编程中,直接在客户端new 接口实现类, 总是会让程序员非常敏感——这意味着强耦合。先明确的告诉大家,如果使用jdk9 提供的服务化会是一个极佳的解决方案,在我们登上“优雅之巅”之前, 先来自己动手去做解耦尝试, 要知道程序员哥哥们的饭碗是“思维能力”,而不是“搬砖”的!任何计算机科学问题都可以通过加一个中间层来解决,ok, 此时应该从大脑的module-path 中迅速定位到一种已经存在的经典的设计模式——工厂模式,工厂模式的目标就是解耦客户端与具体的服务实现,它不就解决恰好解决我们现在的问题了么!? 我们来实现这个工厂:
    it looks like this:

    public class EncryptServiceFactory {
        public static List<String> getSupportedTypes() {
            return List.of(KeyType.RSA.getValue(), KeyType.AES.getValue());
        }
    
        public static DataSecurityService getDataSecurityService(KeyType type) {
            switch (type) {
                case KeyType.RSA:
                    return new RsaEncryptImpl();
                case KeyType.HMAC:
                    return new HmacEncryptImpl();
                default:
                    return new DefaultEncryptImpl();
            }
        }
    }
    

    客户端能够通过工厂方法getSupportedTypes拿到所有支持的加密实现, 并通过工厂方法getDataSecurityService 来获取某一种实现的实例,工厂屏蔽掉了任何客户端对具体实现的感知必要性,服务提供者可以任意修改、扩展,而无需担心服务调用者们。但是我们应该把这个工厂类放到哪里呢?如果把它放到com.dockerx.encrypt.api 模块, 因为工厂类本身是对实现类模块有依赖的,那么将导致com.dockerx.encrypt.api 模块在编译时就需要依赖所有的DataSecurityService 接口实现模块,要知道我们不能再让api 与其具体实现再次耦合! 那我们试试把他独立成一个模块
    看起来就像这样:
    这里写图片描述
    如图所示,工厂模块对接口模块的依赖是requires transitive,所以客户端们现在要在module-info里这样描述:

    module com.dockerx.client {
       requires com.dockerx.encrypt.EncryptServiceFactory;
    }

    对于各种加密实现模块,在module-info中由单纯的exports , 修改为exports xxImpl to com.dockerx.encrypt.factory ,尽可能的向其它模块隐藏实现细节,保持强封装性。
    在客户端会这样调用:

    EncryptServiceFactory.getDataSecurityService("your keyType");

    呀~,看起来怎么变得有点复杂了。。。不过还好,客户端可以开开心心的调用服务了,客户端的module-info 也不再需要requires 任何具体实现类了,可以做到编译时独立于接口实现类模块。不过辩证的看待这个工厂,虽然我们成功借助中间层——工厂解耦了客户端与实现模块,但会发现工厂模块与实现模块还是会存在与之前类似的问题——在编译时需要知道所有实现类。看起来没完没了了。并且每扩展一种实现我们还是要自行去exports ...to ... 实现,至少对工厂模块是可见的,因为工厂还是需要new 实现类。

    jdk9模块间的服务化

    看来是时候拿出我们的终极解决方案了!同样是对一个复杂的问题添加中间层的思想,jdk9 所提供的服务化支持是基于服务注册与服务发现, Service Registry ——服务注册中心,扮演的就是上节工厂的角色,但这个工厂角色由jdk 模块系统底层实现, 所有服务接口类型的各种实现都需要在这里注册,所以说Service Registry 维护了所有可用服务类型的实现信息,就像下面这样:

    这里写图片描述
    现在客户端想要获取一个服务实例,不再需要依赖各种加密具体实现类模块,并且我们砍掉了上节自定义的工厂实现模块,替而代之的是Service Registry,而我们并不需要关注它任何细节,因为整个服务注册与服务发现过程大部分工作都由模块化系统底层来完成。

    服务发布

    回顾我们服务解耦的历程, 我们发现之所以没有搞定具体实现模块与其它模块间的解耦,是不是我们总是会在实现类模块的module-infoexports 自身? 不过看起来好像也没办法,如果不exports模块,其它模块如何消费(requires)此模块? 这也就导致至少存在模块对我们想要隐藏的实现模块是强耦合的。

    不过jdk9 的服务化提供了特殊的支持,它允许服务模块在不需要exports 自身的同时而被其它模块所消费,客户端只需要根据服务接口和Service Registry 通信, 来获取所需的具体服务实现实例,这样完全隔离了服务提供者与消费者(客户端)的联系——没有exports,意味着更强封装性,同时客户端在编译时不需要知道所要消费的服务的任何实现。

    接下来我们尝试继续重构来实现上述特性, 其实我们只需要改动一些模块的module-info 即可。我们上面已经定义了服务接口:

    module com.dockerx.encrypt.api {
        exports security.api;
    }

    它是无论客户端还是服务端都需要共同依赖的模块。
    接下来是我们的服务具体实现类, 仅仅需要修改实现类的module-info

    module com.dockerx.encrypt.RsaImpl{
       requires com.dockerx.encrypt.api;
       provides security.api.EncryptService with security.impl.RsaImpl;

    我们来解释一下provides Awith B 句法, 句法意味着:在此模块,我要为 EncryptService 服务接口(A)提供一种具体实现(B),并将我注册在Service Registry 。没有了exports ,任何其它模块都对其没有可见性,也就实现了隐藏实现,解耦模块的终极目的!至此,简简单单,我们完成了服务注册所要做的全部工作。

    服务发现

    服务发布的 意义在于为消费者提供服务,我们来看如何消费发布在Service Registry 的服务。

    第一步我们需要修改com.dockerx.client 模块的module-info

    module com.dockerx.client{
       requires com.dockerx.encrypt.api;
       uses security.api.EncryptService;

    现在没有了对之前自定义工厂的requires, 使用uses 关键字。 uses 后面跟着服务接口的 全限定包名,表示client会使用EncryptService 服务接口所提供的服务实例, 注意到客户端和服务端(具体实现类) 共同依赖com.dockerx.encrypt.api 模块,而两者之间不存在任何直接或间接依赖,所以uses 语句并不强制在编译时就存在可用服务具体实现,甚至在运行时也不需要任何服务已注册。
    我们已经用uses 声明了对服务的使用,第二步,在客户端代码中使用服务:

    class Client {
        public static void main(String[] args) {
            Iterable<EncryptService> encryptors = ServiceLoader.load(EncryptService.class);
            for (EncryptService encryptor: encryptors) {
                System.out.println(encryptor.encrypt("test str"));
            }
        }
    }

    main 方法内做了两件事:

    1. 首先使用ServiceLoader.load() 创建了一个ServiceLoader 实例
    2. 遍历打印EncryptService 服务接口所有实现

    ServiceLoader 的迭代器,每当迭代到哪一个服务时,对应的服务实现才会实例化, 如果重复来回遍历,那么每一个服务实例会是同一个,意味着服务实例在第一次创建后会被缓存。除非客户端重新调用load 方法后,再次遍历出来的服务才是新的实例。基于这种方式的服务发现,压根就不知道当前遍历到的服务是哪一个模块提供的,而对每一个encryptor实例,效果等价于

    EncryptService encryptor = new RsaImpl();

    我们可以利用jdk8 中提供的,在接口中定义静态方法的特性,来把对ServiceLoader API 的使用“隐藏” 在通用模块com.dockerx.encrypt.api 中。

    interface EncryptService {
        String encrypt(String info);
        static Iterable<EncryptService> getEncryptServices() {
            return ServiceLoader.load(EncryptService.class);
        }
    }

    在客户端main 方法就可以这样来获取服务

    Iterable<EncryptService> encryptors = EncryptService.getEncryptServices(EncryptService.class)

    Tips:
    DI vs jdk9 服务化
    有些同学可能接触过一些企业级框架,比如SpringSpring 内置了一种强大的特性——依赖注入(DI),同样是用来解耦服务的很好实践。/*一种是依赖注入 一种是依赖查找 而非注入/

    另一种服务发布的方式

    发布服务有两种方式:

    • 使用存在无参构造方法的服务接口实现类
    • 使用一个public static 且无参数的provide 方法,其中provide 方法返回一个服务接口类型或实现类

    我们之前的服务发布方式为第一种,而第二种发布方式更为灵活,因为基于无参构造方法,我们没有办法通过传递某些参数来定制化的创建我们想要的服务实例。相反,在第二种方式中, 如何创建服务实例,完全依靠于provide 方法体的具体实现。

    class RsaImpl implements EncryptService {
    
        private String extInfo;
    
        RsaImpl() {
        }
    
        RsaImpl(String extInfo) {
            this.extInfo = extInfo;
        }
    
        public String encrypt(String info) {
            return innerEncrypt(info);
        }
    
        public static RsaImpl provide() {
            return new RsaImpl("created by provide method!")
        }
    }

    provide方法也可以放在其它类里,provide .... with 后面只需要跟上包含provide 方法的全限定名类即可。

    服务选择

    在上面的服务化模型中, 服务端对客户端完全隐藏了具体的实现模块细节, 从客户端的维度,很可能不会用到所有的服务,怎么才能过滤并选择出自己想要使用的服务?我们可以设计一个自描述的服务, 比如我们设计服务接口时新增一个getName() 方法, 用来标志每一个不同的服务实现,客户端就可以在迭代到某个服务时,调用getName() 来识别服务,这样虽然解决了服务识别问题, 但是别忘了, 我们用ServiceLoader API 采用遍历手法时, 在正确找到我们想要的服务实现时,已经在之前初始化了好多我们并不需要的服务实例,万一某个实例的初始化有着很高的性能损耗,譬如需要开辟大量内存,或服务实例不容易GC ,那么这种服务识别方式还是存在潜在问题的,不过jdk9 中的ServiceLoader 已经能够做到在服务实例未创建时做类型检查。ServiceLoader 实例的.stream() 方法返回包含ServiceLoader.Provider 的流。ServiceLoader.Provider 在创建实例之前会检查实现类型。

    所以现在介绍一种新的利用注解来进行服务识别的方法,首先定义一个注解:

    @Retention(RetentionPolicy.RUNTIME)
    public @interface Recommend {
    public boolean value() default true;
    }

    然后把注解放在我们加密实现类上:

    @Recommend("true")
    class RsaImpl implements EncryptService {
    
    }

    现在我们在客户端可以这样来进行服务的选择:

    public class Main {
        public static void main(String args[]) {
            ServiceLoader<Recommend> encryptors=
                ServiceLoader.load(EncryptService.class);
            encryptors.stream()
                .filter(provider -> isRecommend(provider.type()))
                .map(ServiceLoader.Provider::get)
                .forEach(encryptor-> System.out.println(encryptor.getName()));
        }
        private static boolean isRecommend(Class<?> clazz) {
            return clazz.isAnnotationPresent(Recommend.class)
                && clazz.getAnnotation(Recommend.class).value() == true;
        }
    }

    我们现在已经完成了利用注解进行反射来进行服务实现类的识别。

    Tips
    Java.lang.Class vs 访问性
    上面我们是通过反射拿到实现类 的Class 对象,别忘了我们的服务实现类发布服务用的是provider...with..., 并没有导出任何包,看起来是不是破坏了模块化的可访问性?答案是否定的, 既然我们拿到类的class 对象,但如果我们尝试去newInstance() ,但是结果会抛出IllegalAccessError 异常。所以在访问控制上是没有问题的

    四、模块化设计最佳实践

    展开全文
  • Java9模块化简明例子

    2018-10-15 09:31:55
    个人对Java9模块化的理解 个人对Java9模块化的简单理解是:模块是 package 的容器,是 package 的上一层。在模块里,可以输出(管理)当前模块哪些包是对外暴露(exports)的。同样的,当前模块需要别的模块,...
  • 今年,2017年,我们将迎来 Java 语言的 22 岁生日,22岁,对于一个人而言,正是开始大展鸿图的年纪,可是对于日新月异的科技圈中的一门开发语言而言,却是一个傲视群雄的老态龙钟的年纪。 感谢 互联居 的技术分享...
  • 今天来说下模块化 模块化之前出现的问题: Java运行环境的膨胀和臃肿。每次JVM启动的时候,至少会有30~60MB的内存加载,主要原因是JVM需要加载rt.jar,不管其中的类是否被classloader加载,第一步整个jar都会被JVM...
  • 以最近为例,Java 8 以 lambda 表达式和流的形式引入了函数式编程,Java 9 引入了模块化 Java 系统。每个新版本都被热切期待,而一些小的修复补丁则常常被搁在一边,等待更大组件的最终确定。Java 的演变落后于其他...
  • 本文将告诉你学习Java需要达到的30个目标,学习过程中可能遇到的问题,及学习路线。希望能够对你的学习有所帮助。对比一下自己,你已经掌握了这30条中的多少条了呢? 路线 Java发展到现在,按应用来分主要分为三大...
  • JarsLink (原名Titan) 是一个基于JAVA模块化开发框架,它提供在运行时动态加载模块(一个JAR包)、卸载模块和模块间调用的API。也是阿里巴巴的开源项目一 ,目前在微贷事业群广泛使用。需求背景应用拆分的多或少...
  • Dojo之模块化 1定义模块 2调用模块 3解释dojoConfig的全局变量 总结1.引言 在本篇博客中主要讲解一下如何在dojo中使用模块,如何自定义自己的模块等等,在使用dojo编程的时候,大家一定要转变自己的思想,一定要用...
  • 学习Java过程中,慕课网上的一些视频还有一些书籍对我帮助都很大。下面就我自己看过的一些书籍,来分享一些对我帮助比较大的书籍。 1《Head First Java.第二版》 可以说是我的Java启蒙书籍了,特别适合...
  • 模块化4.多版本兼容jar包5.接口方法的改进(在接口中 jdk7 只能声明全名常量和抽象方法 jdk8 添加了静态方法和默认方法 jdk9添加了私有方法)6.钻石操作符升级 可以有{}7.异常处理try升级8.下划线标识符命名的限制 _...
  • 你可能已经听说过 Java 9模块系统,但是这个新版本还有许多其它的更新。 1. Java 平台级模块系统 Java 9 的定义功能是一套全新的模块系统。当代码库越来越大,创建复杂,盘根错节的“意大利面条式代码”的...
  • Java提供了private,public,protected和package private(无修饰符)这四种访问控制级别,不过这仅仅提供了底层的OO数据封装特性。包这个概念确实是起到了分割代码的作用,但是如果包中的代码需要对包外可见,那么...
  • 传智播客于2020年升级了Java学习线路图,硬核升级,免费放送! 学完你就是中级程序员,能更快一步找到工作! 一、Java基础 JavaSE基础是Java中级程序员的起点,是帮助你从小白到懂得编程的必经之路。 在Java...
  • JS模块化编程

    2015-07-14 22:39:03
    然而由于出身的缘故,JS本身在大规模应用上存在着很多问题,比如模块化编程。本文以浏览器端模块化编程为内容,重点介绍AMD规范和CMD规范。在服务器端,NodeJS遵守的Common规范我们这里暂不讨论。  对于计算机语言...
  • 2016最新Java学习计划

    2018-04-02 23:05:53
    一、Java学习路线图 二、Java学习路线图——视频篇 六大阶段学完后目标知识点配套免费资源(视频+笔记+源码+模板)密码 第一阶段Java基础 入门学习周期:35天学完后目标:1.可进行小型应用程序开发2.对数据库进行...
  • 写过一个Java工程师学习指南,我把它分为了四个部分,第一步是打好Java基础,掌握Java核心技术,第二步是掌握Java Web技术栈,能够做一些项目,第三步是掌握Java方面的进阶技术,包括网络编程、并发编程、JVM等,第...
  • Java(JavaEE)学习线路图

    2018-12-19 12:12:59
    Java学习线路图 Java教程 Java 教程 Java 简介 Java 开发环境配置 Java 基础语法 Java 对象和类 Java 基本数据类型 Java变量类型 Java修饰符 Java运算符 Java循环结构 Java分支结构 Java...
1 2 3 4 5 ... 20
收藏数 152,841
精华内容 61,136
热门标签
关键字:

java9学习之模块化