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

Hello World
说了这么多,还是得要动手试一试才能更好的理解,就以官网最简单的例子来做个简单说明。
首先在IDEA创建一个纯Java项目,SDK选择9以上的版本。
然后我们创建主类,写点Hello world代码,添加module-info文件。可以试试引用java.base模块之外的内容,idea会提示你进行依赖处理。注意在这种情况下,module-info只能放在那个位置。
然后我们手动进行编译和打包,打开命令行工具,执行如下命令。注意win下命令会有所不同,以及java版本是否正确。
1 | // 编译处理 |
此时我们可以尝试执行如下命令,都可以得到打印输出:
1 | // 执行编译完class文件 |
对比一下java8和java9 java命令的help说明,--module-path
和-m
都是新出现的选项,可以看到module path和class path是两条路,完全遵从模块化就没有class path老的那一套了。
最后尝试一下JLink,有了这个指令,前文所述的JDK裁切、缩减Java运行时大小才能够达成。JLink是将Module进行打包的工具,创建定制的模块化运行时映像。
1 | $ jlink --module-path $JAVA_HOME/jmods:mlib --add-modules com.greetings --output greetingsapp |
此时我们可以尝试执行如下命令,成功得到打印输出:
1 | $ ./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开发人员来说还是很有必要的。