2

了解Java模块化

 1 year ago
source link: https://www.tony-bro.com/posts/3403681443/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

了解Java模块化

到现在这个时间点,JDK 19已经发布,但是从JDK 9开始出现的、可以说是Java平台破坏性最大的一次改动——模块化(Project Jigsaw),说实话直到这几天之前,我依然不是特别清楚。曾经也有尝试去了解过这个东西,但都不了了之,这可能与我主要是做后端开发有关,确实是没有什么接触这个东西的机会,在Maven、IDE、DevOps的加持下,真的是干就完了。但是看下来它是个值得了解学习的“没用“的知识,感觉应该还是有不少同志有相同的情况,便在此说它一说。

模块化的目的

这里抄一段官网的内容:

  • Make it easier for developers to construct and maintain libraries and large applications;
  • Improve the security and maintainability of Java SE Platform Implementations in general, and the JDK in particular;
  • Enable improved application performance; and
  • Enable the Java SE Platform, and the JDK, to scale down for use in small computing devices and dense cloud deployments.

第一点,在我看来不明显;第二点,安全性、可维护性,源自它提供的更细致的访问控制能力;第三点,比较虚,当然我相信它是有的;第四点,JDK裁切,为存储资源受限的小型设备和云服务赋能。所以我们记住第二点和第四点,这是模块化最核心的内容和目标。

推荐看看大佬对这几个目标的解释:https://www.zhihu.com/question/39112373/answer/79768859

Module Info文件

那什么是模块呢,它可以是说是Java包(Package)的基础上的又一层新的抽象,由Java平台模块系统(JPMS)管理处理。通俗来讲,可以就把它当成一个带有描述文件的Java包,当然了,模块可以由多个Package组成。所以模块的重点就在它的描述文件module-info.java文件中。示例和解释如下:

// 模块名称声明
module sample.module {

// 表明需要依赖、访问某个模块,在模块化的情况下如果要使用其他模块的内容
// 除了java.base这个模块之外都需要显式声明
// 同时,被使用的包也需要在模块中显式声明导出
requires com.another.module;

// 如果依赖只需要在编译期使用,比如测试代码或者一些代码生成库,那么可以选择静态依赖
// 这就和maven的scope provided比较像
requires static com.abc.module;

// 传递型依赖,显然有依赖就会有依赖链,默认情况下依赖不传递,需要显式使用 transitive
requires transitive com.sample.jkl;

// 公开模块下的某个包。默认情况下,模块不会向其他模块公开其任何内容
// 这种强力的封装是模块化能够做到更细的访问控制的关键
exports com.sample.package.abc;

// 限制某个包只开放给指定的其他模块
exports com.sample.package.def to specific.module;

// 为了解耦服务提供方和使用方,模块化提供了use和provides两个关键字
// 这就和SPI机制非常的相似,理解SPI就能理解这个,我觉得也不需要多解释
use com.sample.abcinterface;

// 服务提供方的声明方式,当然你只能with本模块下的内容
provides com.specific.defInterface with com.sample.defImpl;

// 反射API的安全性得到了改进,在模块化下,默认不允许进行反射,需要显式打开反射许可
// 这里我们显式开放了某个包的反射许可
// 如果整个模块都允许反射,那么可以文件开头直接使用 open module sample.module { ... } 的形式
opens com.sample.reflect;

// 同样的,开放反射也可以指定只给到特定的模块
opens com.sample.more to specific.module;

}

从中我们看到,模块描述文件指明了依赖关系、对外暴露控制、反射控制、类SPI服务解耦,public不再意味着能够直接访问,反射也进入了可控范畴。正是这些特质,实现了模块化所带来的更加细致的访问控制和安全性的提升。至于裁切、缩小体积,那是因为JDK本身也做了模块化处理,做到了按需取用,显著减小了Runtime基座的物理体积。

jdk-split.jpg

Hello World

说了这么多,还是得要动手试一试才能更好的理解,就以官网最简单的例子来做个简单说明。

首先在IDEA创建一个纯Java项目,SDK选择9以上的版本。

step1.PNG

然后我们创建主类,写点Hello world代码,添加module-info文件。可以试试引用java.base模块之外的内容,idea会提示你进行依赖处理。注意在这种情况下,module-info只能放在那个位置。

step2.PNG

然后我们手动进行编译和打包,打开命令行工具,执行如下命令。注意win下命令会有所不同,以及java版本是否正确。

// 编译处理
$ mkdir -p mods/com.greetings

$ javac -d mods/com.greetings \
src/module-info.java \
src/com/greetings/Main.java

// 打包处理
$ mkdir mlib

$ jar --create --file=mlib/com.greetings.jar \
--main-class=com.greetings.Main -C mods/com.greetings .
step3.PNG

此时我们可以尝试执行如下命令,都可以得到打印输出:

// 执行编译完class文件
$ java --module-path mods -m com.greetings/com.greetings.Main

// 执行jar包
$ java -p mlib -m com.greetings

对比一下java8和java9 java命令的help说明,--module-path-m都是新出现的选项,可以看到module path和class path是两条路,完全遵从模块化就没有class path老的那一套了。

最后尝试一下JLink,有了这个指令,前文所述的JDK裁切、缩减Java运行时大小才能够达成。JLink是将Module进行打包的工具,创建定制的模块化运行时映像。

$ jlink --module-path $JAVA_HOME/jmods:mlib --add-modules com.greetings --output greetingsapp
step4.PNG

此时我们可以尝试执行如下命令,成功得到打印输出:

$ ./greetingsapp/bin/java -m com.greetings

基本上相当于这个命令把用户模块和需要的JRE模块打包成了一个自定义的JRE,基础库和用户库融合成一体了,这样出来的java命令可以直接执行用户代码。也可以看到打包出来的文件夹大小在40mb左右(根据环境有所不同)。

那为什么说这是“没用”的知识呢,一是因为兼容性足以无缝迁移旧项目,二是在服务端开发场景上用处不多,原来惯有的思维没有改变的动力。虽然说模块化是一个破坏性很“大”的改变,但是Java并没有也不敢让迭代出现太大的裂痕。这就不得不重点说明一下模块化的兼容性,而且理解兼容性可能是一件更重要的事情。

为了保证兼容性,除了正规的module(带有module-info且位于module path下)之外,还有两种特殊的module来为向后兼容或者说辅助迁移提供帮助。

Unnamed Module

每个classloader在classpath下加载的所有JAR(不管是否模块化)共同组成一个unnamed module(未命名模块),未命名模块自动声明依赖所有的显式模块,同时exports自己的所有包,而一个显式模块并不能声明依赖未命名模块。

存在JVM默认选项--illegal-access=permit,即允许unnamed modules反射(java.lang.reflect / java.lang.invoke)使用所有显式模块中的类,但这个不确定java 9之后是不是移除了。

所以,如果我们继续使用传统的classpath方式运行,那就和之前在使用上不会有任何差别,还是public / protected / private那一条,该反射还是能反射。

Automatic Module

模块系统会为在module path上找到的每个JAR包创建一个内部模块,对于模块化的JAR包来说,因为包含了module-info文件,它的模块名、依赖、导出等都是有明确描述的,所以没有什么问题。但是迁移的过程中无法避免非模块化的JAR包依赖。这种情况下模块系统会为它自动创建一个模块,即Automatic module(自动模块),并且对该模块的属性进行最安全的补全。

  • Name:对于模块的名称,如果在MANIFEST文件中定义了Automatic-module-name这个header则以此值为准,否则使用JAR包文件名。
  • Requires:模块系统允许自动模块读取所有其他模块,也就是说自动模块依赖其他所有模块。与其他显式定义的正规模块不同,自动模块可以读取未命名模块。
  • Exports / Opens:模块系统Export、Open Jar包内的所有package

引入模块的原因之一是为了使编译和启动应用程序相比较于classpath形式来说更加可靠、更快地发现错误。为了保持可靠性,模块没有任何办法声明require除了标准模块之外的内容,这其中就包含了从classpath加载的所有东西。如果保持这种状态,那么模块化的JAR只能依赖其他模块化的JAR,这将迫使整个生态系统自底向上全部模块化。很明显这样是不可接受的,因此自动模块作为模块JAR依赖于非模块JAR的一种手段被引入,只要将普通JAR放在模块路径上,并且按照模块系统赋予它的名称进行require就可以正常运转。
另外可以看到,由于自动模块可以读取未命名模块,因此将它的依赖项保留在类路径上是可行的。这样,自动模块就充当了从module到classpath的桥梁。

目前看来模块化的核心作用还是在裁剪JRE体积上,对于客户端开发比较有用,对服务端影响有限。之前没能解决的JAR Hell问题模块化也解决不了,也会有Module Hell。Maven / Gradle这些工具已经能帮助解决绝大多数问题了,在服务端开发上,一来硬件存储基本不在乎这减少的100mb,二来微服务的情况下每个JVM的功能范围都是往小了走的,更加精细的权限控制对编码没有什么正向效果,主要是安全方面。

所以先天动力缺失,在能不动就不懂的“铁原则”下,就算不了解这个东西,照样继续敲代码。本人工作上的服务也基本已经迁移到了Java 11,迁移过程中在代码上基本没什么大的改动。

当然了,这仅是皮毛上的感觉和简介,了解熟悉这个东西对于Java开发人员来说还是很有必要的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK