继 2014 年 3 月 Java 8 发布之后,时隔 4 年,2018 年 9 月,Java 11 如期发布,其间间隔了 Java 9 和 Java 10 两个非LTS(Long Term Support)版本。

作为最新的LTS版本,相比 Java 8,Java 11 包含了模块系统、改用 G1 作为默认 GC 算法、反应式流 Flow、新版 HttpClient 等诸多特性。本文将介绍此次升级最重要的特性——模块系统。

1 模块系统简介

如果把 Java 8 比作单体应用,那么引入模块系统之后,从 Java 9 开始,Java 就华丽的转身为微服务。

模块系统,项目代号 Jigsaw,最早于 2008 年 8 月提出(比 Martin Fowler 提出微服务还早 6 年),2014 年跟随 Java 9 正式进入开发阶段,最终跟随 Java 9 发布于 2017 年 9 月。

那么什么是模块系统?

官方的定义是A uniquely named, reusable group of related packages, as well as resources (such as images and XML files) and a module descriptor.如图-1_所示,模块的载体是 jar 文件,一个模块就是一个 jar 文件,但相比于传统的 jar 文件,模块的根目录下多了一个 module-info.class 文件,也即 module descriptor

module descriptor 包含以下信息:

  • 模块名称

  • 依赖哪些模块

  • 导出模块内的哪些包(允许直接 import 使用)

  • 开放模块内的哪些包(允许通过 Java 反射访问)

  • 提供哪些服务

  • 依赖哪些服务

图-1: Java 9 Module

也就是说,任意一个 jar 文件,只要加上一个合法的 module descriptor,就可以升级为一个模块。关注公众号Java技术栈在后台回复Java获取一系列Java新特性教程。

这个看似微小的改变,到底可以带来哪些好处?在我看来,至少带来四方面的好处。

第一,原生的依赖管理。

有了模块系统,Java 可以根据 module descriptor 计算出各个模块间的依赖关系,一旦发现循环依赖,启动就会终止。同时,由于模块系统不允许不同模块导出相同的包(即 split package,分裂包),所以在查找包时,Java 可以精准的定位到一个模块,从而获得更好的性能。

第二,精简 JRE。

引入模块系统之后,JDK 自身被划分为 94 个模块(参见_图-2_)。通过 Java 9 新增的 jlink 工具,开发者可以根据实际应用场景随意组合这些模块,去除不需要的模块,生成自定义 JRE,从而有效缩小 JRE 大小。

得益于此,JRE 11 的大小仅为 JRE 8 的 53%,从 218.4 MB缩减为 116.3 MB,JRE 中广为诟病的巨型 jar 文件 rt.jar 也被移除。更小的 JRE 意味着更少的内存占用,这让 Java 对嵌入式应用开发变得更友好。

图-2: The Modular JDK

第三,更好的兼容性。

自打 Java 出生以来,就只有 4 种包可见性,这让 Java 对面向对象的三大特征之一封装的支持大打折扣,类库维护者对此叫苦不迭,只能一遍又一遍的通过各种文档或者奇怪的命名来强调这些或者那些类仅供内部使用,擅自使用后果自负云云。

Java 9 之后,利用 module descriptor 中的 exports 关键词,模块维护者就精准控制哪些类可以对外开放使用,哪些类只能内部使用,换句话说就是不再依赖文档,而是由编译器来保证。类可见性的细化,除了带来更好的兼容性,也带来了更好的安全性。

图-3: Java Accessibility

第四,提升 Java 语言开发效率。

Java 9 之后,Java 像开挂了一般,一改原先一延再延的风格,严格遵循每半年一个大版本的发布策略,从 2017 年 9 月到 2020 年 3 月,从 Java 9 到 Java 14,三年时间相继发布了 6 个版本,无一延期,参见图4。

这无疑跟模块系统的引入有莫大关系。前文提到,Java 9 之后,JDK 被拆分为 94 个模块,每个模块有清晰的边界(module descriptor)和独立的单元测试,对于每个 Java 语言的开发者而言,每个人只需要关注其所负责的模块,开发效率因此大幅提升。这其中的差别,就好比单体应用架构升级到微服务架构一般,版本迭代速度不快也难。

图-4: Java SE Lifecycle

2 基础篇

2.1 module descriptor

上面提到,模块的核心在于 module descriptor,对应根目录下的 module-info.class 文件,而这个 class 文件是由源代码根目录下的 module-info.java 编译生成。

Java 为 module-info.java 设计了专用的语法,包含 module、 requiresexports 等多个关键词(参见_图-5_)。

图-5: module-info.java 语法

语法解读:

  • [open] module <module>: 声明一个模块,模块名称应全局唯一,不可重复。加上 open 关键词表示模块内的所有包都允许通过 Java 反射访问,模块声明体内不再允许使用 opens 语句。

  • requires [transitive] <module>: 声明模块依赖,一次只能声明一个依赖,如果依赖多个模块,需要多次声明。加上 transitive 关键词表示传递依赖,比如模块 A 依赖模块 B,模块 B 传递依赖模块 C,那么模块 A 就会自动依赖模块 C,类似于 Maven。

  • exports <package> [to <module1>[, <module2>...]]: 导出模块内的包(允许直接 import 使用),一次导出一个包,如果需要导出多个包,需要多次声明。如果需要定向导出,可以使用 to 关键词,后面加上模块列表(逗号分隔)。

  • opens <package> [to <module>[, <module2>...]]: 开放模块内的包(允许通过 Java 反射访问),一次开放一个包,如果需要开放多个包,需要多次声明。如果需要定向开放,可以使用 to 关键词,后面加上模块列表(逗号分隔)。

  • provides <interface | abstract class> with <class1>[, <class2> ...]: 声明模块提供的 Java SPI 服务,一次可以声明多个服务实现类(逗号分隔)。

  • uses <interface | abstract class>: 声明模块依赖的 Java SPI 服务,加上之后模块内的代码就可以通过 ServiceLoader.load(Class) 一次性加载所声明的 SPI 服务的所有实现类。

2.2 -p & -m 参数

Java 9 引入了一系列新的参数用于编译和运行模块,其中最重要的两个参数是 -p 和 -m-p 参数指定模块路径,多个模块之间用 “:”(Mac, Linux)或者 “;”(Windows)分隔,同时适用于 javac 命令和 java 命令,用法和Java 8 中的 -cp 非常类似。-m 参数指定待运行的模块主函数,输入格式为模块名/主函数所在的类名,仅适用于 java 命令。两个参数的基本用法如下:

  • javac -p <module_path> <source>

  • java -p <module_path> -m <module>/<main_class>

2.3 Demo 示例

为了帮助你理解 module descriptor 语法和新的 Java 参数,我专门设计了一个示例工程,其内包含了 5 个模块:

  • mod1 模块: 主模块,展示了使用服务实现类的两种方式。

  • mod2a 模块: 分别导出和开放了一个包,并声明了两个服务实现类。

  • mod2b 模块: 声明了一个未公开的服务实现类。

  • mod3 模块: 定义 SPI 服务(IEventListener),并声明了一个未公开的服务实现类。

  • mod4 模块: 导出公共模型类。

图-6: 包含 5 个模块的示例工程

先来看一下主函数,方式 1 展示了直接使用 mod2 导出和开放的两个 IEventListener 实现类,方式 2 展示了通过 Java SPI 机制使用所有的 IEventListener 实现类,无视其导出/开放与否。

方式 2 相比 方式 1,多了两行输出,分别来自于 mod2b 和 mod3 通过 provides 关键词提供的服务实现类。

public class EventCenter {public static void main(String[] args) throws ReflectiveOperationException {System.out.println("Demo: Direct Mode");var listeners = new ArrayList<IEventListener>();listeners.add(new EchoListener());listeners.add((IEventListener<String>) Class.forName("mod2a.opens.ReflectEchoListener").getDeclaredConstructor().newInstance());var event = Events.newEvent();listeners.forEach(l -> l.onEvent(event));System.out.println();System.out.println("Demo: SPI Mode");var listeners2 = ServiceLoader.load(IEventListener.class).stream().map(ServiceLoader.Provider::get).collect(Collectors.toList());var event2 = Events.newEvent();listeners2.forEach(l -> l.onEvent(event2));}
}

代码-1: mod1.EventCenter.java

命令行下执行./build_mods.sh,得到输出如下,结果和预期一致。

Demo: Direct Mode
[echo] Event received: 68eb4671-c057-4bc2-9653-c31f5e3f72d2
[reflect echo] Event received: 68eb4671-c057-4bc2-9653-c31f5e3f72d2Demo: SPI Mode
[spi echo] Event received: 678d239a-77ef-4b7f-b7aa-e76041fcdf47
[echo] Event received: 678d239a-77ef-4b7f-b7aa-e76041fcdf47
[reflect echo] Event received: 678d239a-77ef-4b7f-b7aa-e76041fcdf47
[internal echo] Event received: 678d239a-77ef-4b7f-b7aa-e76041fcdf47

代码-2: EventCenter 结果输出

3 进阶篇

看到这里,相信创建和运行一个新的模块应用对你而言已经不是问题了,可问题是老的 Java 8 应用怎么办?

别着急,我们先来了解两个高级概念,未命名模块(unnamed module)和自动模块(automatic module)。

图-7: 未命名模块 vs 自动模块

一个未经模块化改造的 jar 文件是转为未命名模块还是自动模块,取决于这个 jar 文件出现的路径,如果是类路径,那么就会转为未命名模块,如果是模块路径,那么就会转为自动模块。

注意,自动模块也属于命名模块的范畴,其名称是模块系统基于 jar 文件名自动推导得出的,比如 com.foo.bar-1.0.0.jar 文件推导得出的自动模块名是 com.foo.bar。

图-7_列举了未命名模块和自动模块行为上的区别,除此之外,两者还有一个关键区别,分裂包规则适用于自动模块,但对未命名模块无效,也即多个未命名模块可以导出同一个包,但自动模块不允许。

未命名模块和自动模块存在的意义在于,无论传入的 jar 文件是否一个合法的模块(包含 module descriptor),Java 内部都可以统一的以模块的方式进行处理,这也是 Java 9 兼容老版本应用的架构原理。

运行老版本应用时,所有 jar 文件都出现在类路径下,也就是转为未命名模块,对于未命名模块而言,默认导出所有包并且依赖所有模块,因此应用可以正常运行。进一步的解读可以参阅官方白皮书的相关章节。

基于未命名模块和自动模块,相应的就产生了两种老版本应用的迁移策略,或者说模块化策略。

3.1 Bottom-up 自底向上策略

第一种策略,叫做自底向上(bottom-up)策略,即根据 jar 包依赖关系(如果依赖关系比较复杂,可以使用 jdeps 工具进行分析),沿着依赖树自底向上对 jar 包进行模块化改造(在 jar 包的源代码根目录下添加合法的模块描述文件 module-info.java)。

初始时,所有 jar 包都是非模块化的,全部置于类路径下(转为未命名模块),应用以传统方式启动。然后,开始自底向上对 jar 包进行模块化改造,改造完的 jar 包就移到模块路径下,这期间应用仍以传统方式启动。

最后,等所有 jar 包都完成模块化改造,应用改为 -m 方式启动,这也标志着应用已经迁移为真正的 Java 9 应用。

以上面的示例工程为例

图-8: Bottom-up模块化策略
  1. 假设初始时,所有 jar 包都是非模块化的,此时应用运行命令为:

java -cp mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar mod1.EventCenter

  1. 对 mod3 和 mod4 进行模块化改造。完成之后,此时 mod1, mod2a, mod2b 还是普通的 jar 文件,新的运行命令为:

java -cp mod1.jar:mod2a.jar:mod2b.jar -p mod3.jar:mod4.jar --add-modules mod3,mod4 mod1.EventCenter

对比上一步的命令,首先 mod3.jar 和 mod4.jar 从类路径移到了模块路径,这个很好理解,因为这两个 jar 包已经改造成了真正的模块。其次,多了一个额外的参数 --add-modules mod3,mod4,这是为什么呢?这就要谈到模块系统的模块发现机制了。

不管是编译时,还是运行时,模块系统首先都要确定一个或者多个根模块(root module),然后从这些根模块开始根据模块依赖关系在模块路径中循环找出所有可观察到的模块(observable module),这些可观察到的模块加上类路径下的 jar 文件最终构成了编译时环境和运行时环境。

那么根模块是如何确定的呢?对于运行时而言,如果应用是通过 -m 方式启动的,那么根模块就是 -m 指定的主模块;如果应用是通过传统方式启动的,那么根模块就是所有的 java.* 模块即 JRE(参见_图-2_)。

回到前面的例子,如果不加 --add-modules 参数,那么运行时环境中除了 JRE 就只有 mod1.jar、mod2a.jar、mod2b.jar,没有 mod3、mod4 模块,就会报 java.lang.ClassNotFoundException 异常。如你所想,--add-modules 参数的作用就是手动指定额外的根模块,这样应用就可以正常运行了。

  1. 接着完成 mod2a、mod2b 的模块化改造,此时运行命令为:

java -cp mod1.jar -p mod2a.jar:mod2b.jar:mod3.jar:mod4.jar --add-modules mod2a,mod2b,mod4 mod1.EventCenter

由于 mod2a、mod2b 都依赖 mod3,所以 mod3 就不用加到 --add-modules 参数里了。

  1. 最后完成 mod1 的模块化改造,最终运行命令就简化为:

java -p mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar -m mod1/mod1.EventCenter

注意此时应用是以 -m 方式启动,并且指定了 mod1 为主模块(也是根模块),因此所有其他模块根据依赖关系都会被识别为可观察到的模块并加入到运行时环境,应用可以正常运行。

3.2 Top-down 自上而下策略

自底向上策略很容易理解,实施路径也很清晰,但它有一个隐含的假设,即所有 jar 包都是可以模块化的,那如果其中有 jar 包无法进行模块化改造(比如 jar 包是一个第三方类库),怎么办?别慌,我们再来看第二种策略,叫做自上而下(top-down)策略。

它的基本思路是,根据 jar 包依赖关系,从主应用开始,沿着依赖树自上而下分析各个 jar 包模块化改造的可能性,将 jar 包分为两类,一类是可以改造的,一类是无法改造的。

对于第一类,我们仍然采用自底向上策略进行改造,直至主应用完成改造,对于第二类,需要从一开始就放入模块路径,即转为自动模块。

这里就要谈一下自动模块设计的精妙之处,首先,自动模块会导出所有包,这样就保证第一类 jar 包可以照常访问自动模块,其次,自动模块依赖所有命名模块,并且允许访问所有未命名模块的类(这一点很重要,因为除自动模块之外,其它命名模块是不允许访问未命名模块的类),这样就保证自动模块自身可以照常访问其他类。等到主应用完成模块化改造,应用的启动方式就可以改为 -m 方式。

还是以示例工程为例,假设 mod4 是一个第三方 jar 包,无法进行模块化改造,那么最终改造完之后,虽然应用运行命令和之前一样还是java -p mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar -m mod1/mod1.EventCenter,但其中只有 mod1、mod2a、mod2b、mod3 是真正的模块,mod4 未做任何改造,借由模块系统转为自动模块。

图-9: Top-down模块化策略

看上去很完美,不过等一下,如果有多个自动模块,并且它们之间存在分裂包呢?

前面提到,自动模块和其它命名模块一样,需要遵循分裂包规则。对于这种情况,如果模块化改造势在必行,要么忍痛割爱精简依赖只保留其中的一个自动模块,要么自己动手丰衣足食 Hack 一个版本。当然,你也可以试试找到这些自动模块的维护者们,让他们 PK 一下决定谁才是这个分裂包的主人。

4 番外篇

有关模块系统的介绍到这就基本结束了,简单回顾一下,首先我介绍了什么是模块、模块化的好处,接着给出了定义模块的语法,和编译、运行模块的命令,并辅以一个示例工程进行说明,最后详细阐述了老版本应用模块化改造的思路。

现在我们再来看一些跟模块系统比较相似的框架和工具,以进一步加深你对模块系统的理解。

4.1 vs OSGi

说起模块化,尤其在 Java 界,那么肯定绕不过 OSGi 这个模块系统的鼻祖。OSGi 里的 bundle 跟模块系统里的模块非常相似,都是以 jar 文件的形式存在,每个 bundle 有自己的名称,也会定义依赖的 bundle、导出的包、发布的服务等。所不同的是,OSGi bundle 可以定义版本,还有生命周期的概念,包括 installed、resolved、uninstalled、starting、active、stopping 6 种状态,所有 bundle 都由 OSGi 容器进行管理,并且在同一个 OSGi 容器里面允许同时运行同一个 bundle 的多个版本,甚至每个 bundle 有各自独立的 classloader。以上种种特性使得 OSGi 框架变得非常重,在微服务盛行的当下,越来越被边缘化。

4.2 vs Maven

Maven 的依赖管理和模块系统存在一些相似之处,Maven 里的 artifact 对应模块 ,都是以 jar 文件的形式存在,有名称,可以声明传递依赖。不同之处在于,Maven artifact 支持版本,但缺少包一级的信息,也没有服务的概念。如果 Java 一出生就带有模块系统,那么 Maven 的依赖管理大概率就会直接基于模块系统来设计了。

4.3 vs ArchUnit

ArchUnit 在包可见性方面的控制能力和模块系统相比,有过之而无不及,并且可以细化到类、方法、属性这一级。但 ArchUnit 缺少模块一级的控制,模块系统的出现正好补齐了 ArchUnit 这一方面的短板,两者相辅相成、相得益彰,以后落地架构规范也省了很多口水。

5 彩蛋

如果你能看到这里,恭喜你已经赢了 90% 的读者。为了表扬你的耐心,免费赠送一个小彩蛋,给你一个 jar 文件,如何用最快的速度判别它是不是一个模块?它又是如何定义的?试试看 jar -d -f <jar_file>

有关 Java 模块系统的介绍就到这里了,欢迎你到我的留言板分享,和大家一起过过招。更多请关注公众号Java技术栈在后台回复Java获取一系列Java新特性教程,下期再见。

版权声明:本文为CSDN博主「eMac」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/eMac/article/details/107131444

6 参考

https://www.ibm.com/developerworks/cn/java/the-new-features-of-Java-9/index.html)

https://openjdk.java.net/jeps/261

http://tutorials.jenkov.com/java/modules.html

https://www.oracle.com/corporate/features/understanding-java-9-modules.html

https://www.oracle.com/java/java9-screencasts.html

https://www.cnblogs.com/IcanFixIt/p/6947763.html

从 Java 9 开始,Java 就华丽的转身为微服务了…相关推荐

  1. java调用webservice_笃学私教:Java开发网站架构演变过程-从单体应用到微服务架构详解...

    原标题:笃学私教:Java开发网站架构演变过程-从单体应用到微服务架构详解 Java开发网站架构演变过程,到目前为止,大致分为5个阶段,分别为单体架构.集群架构.分布式架构.SOA架构和微服务架构.下 ...

  2. java event sourcing_使用Spring Cloud和Reactor在微服务中实现EventSourcing -解道Jdon

    使用Spring Cloud和Reactor在微服务中实现Event Sourcing 当在微服务架构中构建应用时,状态管理成为分布式系统的问题,相比于传统monolithic应用,将状态管理通过事务 ...

  3. 499、Java分布式和集群12 -【SpringCloud视图微服务 - 消息总线Bus】 2021.06.01

    目录 0.RabbitMQ 1.先运行,看到效果,再学习 2.pom.xml 3.bootstrap.yml 4.application.yml 5.ProductDataServiceApplica ...

  4. 平安银行Java社招五面面经:MQ+微服务+JVM+redis(已拿offer)

    平安银行一面: 自我介绍,聊项目, 这边挺常规的,从项目入手. 解析Excel为什么用POI ? POI存在的问题? 还知道哪些解析Excel的工具吗? 你们项目中用过MQ,平时都用MQ来做什么? 你 ...

  5. 看呆了!二面高德 Java 岗,问了一堆源码,微服务,分布式,Redis,心累

    前段时间一个粉丝去面试阿里旗下的高德地图,哇,那叫一个心累啊- 第一轮面试下来还算 OK,基本上问的都是一些基础性的东西,到了第二轮面试,面试官问了一堆什么源码啊,中间件,分布式这些东西,越答越懵逼- ...

  6. 「Java分享客栈」随时用随时翻:微服务链路追踪之zipkin搭建

    前言 微服务治理方案中,链路追踪是必修课,SpringCloud的组件其实使用很简单,生产环境中真正令人头疼的往往是软件维护,接口在微服务间的调用究竟哪个环节出现了问题,哪个环节耗时较长,这都是项目上 ...

  7. Java面试题大全(持续更新中,微服务架构视频教程

    []( )9.成员变量和局部变量的区别 A:在类中的位置不同 成员变量:在类中方法外 局部变量:在方法定义中或者方法声明上 B:在内存中的位置不同 成员变量:在堆内存(成员变量属于对象,对象进堆内存) ...

  8. Java程序员必经的实践之路:微服务与SOA架构

    而另一方面,微服务推崇执行的标准(例如HTTP)却是人们广泛了解并共同使用的.我们可以通过选择合适的语言或工具来构建某个组件微服务.SOA与微服务还有一个更大的区别:领域模型.在基于微服务的软件中,每 ...

  9. 微服务:Java EE的拯救者还是掘墓人?

    有人认为,微服务的大行其道是在给Java EE下达死刑判决书.也有人认为,Java EE已死的论调可笑至极.读者朋友,你们怎么看? 引言 有人说,Java确实过于臃肿,经常"小题大做&quo ...

最新文章

  1. STM32控制OLCD显示中英文(NB-IoT专栏—基础篇6)
  2. Python函数01/函数的初识/函数的定义/函数调用/函数的返回值/函数的参数
  3. 发现四川科技馆在线网站修改用户设置页面的一个问题
  4. 租金 预测_如何预测租金并优化租赁期限,从而节省资金
  5. VC嵌入python时debug版lib下载
  6. Android官方开发文档Training系列课程中文版:后台加载数据之使用CursorLoader进行查询
  7. 1203. 项目管理
  8. 推荐一个Silverlight多文件(大文件)上传的开源项目(转载)
  9. 媲美Siri语音 英朗自然语音识别系统体验
  10. 文章发送到多平台软件:融媒宝
  11. L0到L4超全介绍!30+自动驾驶方案汇总
  12. 蓝桥杯 扶老奶奶过街 (逻辑推理)
  13. 大学毕业标语计算机学院,大学毕业季横幅标语
  14. CSDN文章转PDF
  15. shell脚本:遍历指定文件夹下.jar后缀的文件,并备份到目标文件夹
  16. 怎么把MySQL数据库卸载干净
  17. com.android.backupconfirm,使用ADB停用一些自己讨厌的东西记录备忘
  18. 不仅仅是“屠龙之技”-编译器开发者的精神胜利!
  19. 【转】Jperf2.0下载及使用方法介绍
  20. 【IVI】1. Android Automotive OS 安卓车载操作系统白皮书

热门文章

  1. 信息系统项目管理师-战略管理知识点
  2. 杰出数据科学家的关键技能是什么?
  3. SpringBoot同时集成Redis和Guava作为缓存组件--进一步分析代码
  4. WEB服务器、应用程序服务器、HTTP服务器的区别
  5. 一枚菜狗子的2016总结
  6. therefore/so/hence/then/accordingly/Thus
  7. 【java】静态代码块
  8. JS Math对象中一些小技巧
  9. 【Python】pycharm去掉代码下的波浪线
  10. iOS下音视频通信-基于WebRTC