阶行 大淘宝技术 2024年07月05日 18:57 浙江
Java平台从Java 8向Java 9及更高版本的进化,其中引入了一个重要的新特性——模块系统(Project Jigsaw)。模块系统的目的是解决大型应用的依赖管理问题,提升性能,简化JRE,增强兼容性和安全性,并提高开发效率。通过模块化,Java能够更好地支持微服务架构,提供更细粒度的封装和控制,以及更清晰的依赖关系。文章详细介绍了模块系统的概念,如MODULE DESCRIPTOR、主要参数、关键指令,以及模块化策略。此外,本文还提供了最佳实践建议,帮助开发者更好地理解和应用Java模块系统。
模块系统简介
如果把 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.
如图所示,模块的载体是 jar 文件,一个模块就是一个 jar 文件,但相比于传统的 jar 文件,模块的根目录下多了一个 module-info.class 文件,也即 module descriptor,包含信息:模块名称、依赖哪些模块、导出模块内的哪些包[允许直接 import 使用]、开放模块内的哪些包[允许通过 Java 反射访问]、提供哪些服务、依赖哪些服务。
任意一个 jar 文件,只要加上一个合法的 module descriptor,就可以升级为一个模块。这个看似微小的改变,在我看来,至少带来四方面的好处:
一、清晰的依赖管理:
二、精简 JRE:
引入模块系统之后,JDK 自身被划分为 94 个模块(参见图)。通过 Java 9 新增的 jlink 工具,开发者可以根据实际应用场景随意组合这些模块,去除不需要的模块,生成自定义 JRE,从而有效缩小 JRE 大小。得益于此,JRE 11 的大小仅为 JRE 8 的 53%,从 218.4 MB缩减为 116.3 MB,JRE 中广为诟病的巨型 jar 文件 rt.jar 也被移除。更小的 JRE 意味着更少的内存占用,这让 Java 对嵌入式应用开发变得更友好。
三、更好的兼容性 & 安全性:
Java一直以来,就只有 4 种包可见性,这让 Java 对面向对象的三大特征之一封装的支持大打折扣,类库维护者对此叫苦不迭,只能一遍又一遍的通过各种文档或者奇怪的命名来强调这些或者那些类仅供内部使用,擅自使用后果自负云云。Java 9 之后,利用 module descriptor 中的 exports 关键词,模块维护者就精准控制哪些类可以对外开放使用,哪些类只能内部使用,换句话说就是不再依赖文档,而是由编译器来保证。类可见性的细化,除了带来更好的兼容性,也带来了更好的安全性。
四、提升 Java 语言开发效率:
Java 9 之后,Java 像开挂了一般,一改原先一延再延的风格,严格遵循每半年一个大版本的发布策略,从 2017 年 9 月到 2020 年 3 月,从 Java 9 到 Java 14,三年时间相继发布了 6 个版本,无一延期,参见图-4。这无疑跟模块系统的引入有莫大关系。前文提到,Java 9 之后,JDK 被拆分为 94 个模块,每个模块有清晰的边界(module descriptor)和独立的单元测试,对于每个 Java 语言的开发者而言,每个人只需要关注其所负责的模块,开发效率因此大幅提升。这其中的差别,就好比单体应用架构升级到微服务架构一般,版本迭代速度不快也难。
核心概念
模块的核心在于 module descriptor(模块描述符),对应根目录下的 module-info.class 文件,而这个 class 文件是由源代码根目录下的 module-info.java 编译生成。Java 为 module-info.java 设计了专用的语法,包含 module、 requires、exports 等多个关键词。
module-info.java:
import mod3.exports.IEventListener;module mod1 { requires mod2a; requires mod4; uses IEventListener;}
EventCenter.java(主函数):
package mod1;import mod2a.exports.EchoListener;import mod3.exports.IEventListener;import mod4.Events;import java.util.ArrayList;import java.util.ServiceLoader;import java.util.stream.Collectors;public class EventCenter { // 方式1:通过exports和opens System.out.println("Demo: Direct Mode"); var listeners = new ArrayList<IEventListener>(); // 使用导出类 listeners.add(new EchoListener()); // 使用开放类 // compile error: listeners.add(new ReflectEchoListener()); listeners.add((IEventListener<String>) Class.forName("mod2a.opens.ReflectEchoListener").getDeclaredConstructor().newInstance()); var event = Events.newEvent(); listeners.forEach(l -> l.onEvent(event)); System.out.println(); // 方式2:通过SPI System.out.println("Demo: SPI Mode"); // 加载所有的IEventListener实现类,无视其导出/开放与否 var listeners2 = ServiceLoader.load(IEventListener.class).stream().map(ServiceLoader.Provider::get).collect(Collectors.toList()); // compile error: listeners.add(new InternalEchoListener()); // compile error: listeners.add(new SpiEchoListener()); var event2 = Events.newEvent(); listeners2.forEach(l -> l.onEvent(event2));}
shell 执行脚本:
#!/bin/zsh# mod4javac -d out/mods/mod4 mod4/src/**/*.javajar -cvf out/mods/mod4.jar -C out/mods/mod4 .# mod3javac -d out/mods/mod3 mod3/src/**/*.javajar -cvf out/mods/mod3.jar -C out/mods/mod3 .# mod2bjavac -p out/mods/mod3.jar -d out/mods/mod2b mod2b/src/**/*.javajar -cvf out/mods/mod2b.jar -C out/mods/mod2b .# mod2ajavac -p out/mods/mod3.jar -d out/mods/mod2a mod2a/src/**/*.javajar -cvf out/mods/mod2a.jar -C out/mods/mod2a .# mod1javac -p out/mods/mod2a.jar:out/mods/mod2b.jar:out/mods/mod3.jar:out/mods/mod4.jar -d out/mods/mod1 mod1/src/**/*.javajar -cvf out/mods/mod1.jar -C out/mods/mod1 .# runjava -p out/mods/mod1.jar:out/mods/mod2a.jar:out/mods/mod2b.jar:out/mods/mod3.jar:out/mods/mod4.jar -m mod1/mod1.EventCenter
module-info.java:
import mod3.exports.IEventListener;module mod2a { requires transitive mod3; exports mod2a.exports; opens mod2a.opens; provides IEventListener with mod2a.exports.EchoListener, mod2a.opens.ReflectEchoListener;}
EchoListener.java & ReflectEchoListener.java:
package mod2a.exports;import mod3.exports.IEventListener;public class EchoListener implements IEventListener<String> { @Override public void onEvent(String event) { System.out.println("[echo] Event received: " + event); }}package mod2a.opens;import mod3.exports.IEventListener;public class ReflectEchoListener implements IEventListener<String> { @Override public void onEvent(String event) { System.out.println("[reflect echo] Event received: " + event); }}
module-info.java:
import mod3.exports.IEventListener;module mod2b { requires transitive mod3; provides IEventListener with mod2b.SpiEchoListener;}
SpiEchoListener.java:
package mod2b;import mod3.exports.IEventListener;public class SpiEchoListener implements IEventListener<String> { @Override public void onEvent(String event) { System.out.println("[spi echo] Event received: " + event); }}
module-info.java:
import mod3.exports.IEventListener;module mod1 { requires mod2a; requires mod4; uses IEventListener;}
IEventListener.java & InternalEchoListener.java:
package mod3.exports;public interface IEventListener<T> { void onEvent(T event);}package mod3.internal;import mod3.exports.IEventListener;public class InternalEchoListener implements IEventListener<String> { @Override public void onEvent(String event) { System.out.println("[internal echo] Event received: " + event); }}
module-info.java:
module mod4 { exports mod4;}
Events.java:
package mod4;import java.util.UUID;public class Events { public static String newEvent() { return UUID.randomUUID().toString(); }}
我们可以使用 --patch-module来修补分割:
java --add-modules java.xml.ws.annotation --patch-module java.xml.ws.annotation=jsr305-3.0.2.jar --class-path $dependencies -jar $appjar
模块化策略
兼容老版本的应用,先来了解两个高级概念:未命名模块(unnamed module)和 自动模块(automatic module)
判断:一个未经模块化改造的 jar 文件是转为未命名模块还是自动模块,取决于这个 jar 文件出现的路径,如果是类路径,那么就会转为未命名模块,如果是模块路径,那么就会转为自动模块。
未命名模块和自动模块存在的意义在于,无论传入的 jar 文件是否一个合法的模块(包含 module descriptor),Java 内部都可以统一的以模块的方式进行处理,这也是 Java 9 兼容老版本应用的架构原理。运行老版本应用时,所有 jar 文件都出现在类路径下,也就是转为未命名模块,对于未命名模块而言,默认导出所有包并且依赖所有模块,因此应用可以正常运行。进一步的解读可以参阅官方白皮书的相关章节。
对比上一步的命令,首先 mod3.jar 和 mod4.jar 从类路径移到了模块路径,这个很好理解,因为这两个 jar 包已经改造成了真正的模块。其次,多了一个额外的参数 --add-modules mod3,mod4,这是为什么呢?这就要谈到模块系统的模块发现机制了。
不管是编译时,还是运行时,模块系统首先都要确定一个或者多个根模块(root module),然后从这些根模块开始根据模块依赖关系在模块路径中循环找出所有可观察到的模块(observable module),这些可观察到的模块加上类路径下的 jar 文件最终构成了编译时环境和运行时环境。那么根模块是如何确定的呢?对于运行时而言,如果应用是通过 -m 方式启动的,那么根模块就是 -m 指定的主模块;如果应用是通过传统方式启动的,那么根模块就是所有的 java.* 模块即 JRE。回到前面的例子,如果不加 --add-modules 参数,那么运行时环境中除了 JRE 就只有 mod1.jar、mod2a.jar、mod2b.jar,没有 mod3、mod4 模块,就会报 java.lang.ClassNotFoundException 异常。如你所想,--add-modules 参数的作用就是手动指定额外的根模块,这样应用就可以正常运行了。
最佳实践
参考资料