Java9引入的模块化系统(ProjectJigsaw)是Java平台历史上最重大的结构性变更之一。它解决了长期困扰Java开发者的两个核心问题:JDK本身的过度膨胀,以及类路径(Classpath)上的JAR地狱。模块化不仅改变了JDK的组织方式,也为大型应用提供了更强的封装性和可伸缩性。
参考:https://oqmyh.cn/category/meirong-zhishi.html
在Java9之前,JDK的运行时镜像(rt.jar)超过60MB,包含数千个类,即使只使用其中一小部分,整个运行时也必须完整加载。模块化将JDK拆分为大约100个模块,应用只需要包含实际使用的模块。jlink工具可以创建自定义运行时镜像,大幅减少内存占用和启动时间,这对容器化部署和云原生应用意义重大。
模块化的核心是module-info.java文件,它声明模块的名称、依赖(requires)、导出包(exports)、开放包(opens,用于反射)、提供服务(provides)和使用服务(uses)。模块系统在编译期和运行期强制执行这些声明,防止意外访问内部API。
强封装是模块化最重要的特性。在传统Java中,public意味着完全公开,模块化引入了“可访问性”的新层次:一个包即使被声明为public,如果模块没有导出该包,其他模块也无法访问。这使API设计者可以真正隐藏实现细节,防止用户依赖内部类。
服务加载机制(ServiceLoader)在模块化时代得到了增强。模块可以使用provides...with...声明服务提供者,使用uses声明服务消费者。模块系统自动将消费者绑定到提供者,无需配置文件。这种松耦合的依赖注入是模块化的最佳实践。
参考:https://npqev.cn/category/xianhua-pinzhong.html
JDK内部API的封装是模块化的直接影响。Java9之前,许多应用依赖sun.misc.Unsafe、sun.reflect等内部API。模块化将这些API封装在非导出包中,访问它们会抛出IllegalAccessError。开发者必须迁移到标准API(如VarHandle、MethodHandles),或使用--add-exports命令行参数临时解封。
模块路径vs类路径:传统类路径是扁平的JAR列表,JVM按顺序搜索。模块路径上的模块支持强封装、可靠配置和服务绑定。传统JAR放在类路径上时,成为“未命名模块”,可以访问所有模块(因为未命名模块读取所有模块),但不能被其他模块访问(除非使用--add-reads)。这种设计允许增量迁移。
自动模块:将传统JAR放在模块路径上但不包含module-info.class,就成为自动模块。自动模块的名称从JAR文件名推断,它导出所有包,并依赖所有其他模块。自动模块是迁移过程中的过渡机制,最终应该为每个JAR添加module-info.java。
多版本JAR:允许同一个JAR包含针对不同Java版本的类。例如,META-INF/versions/9/下的类在Java9+上使用,其他版本使用根目录的类。这使库可以支持模块化,同时保持对旧Java版本的兼容性。
模块化迁移策略:对于大型代码库,一次性迁移是不现实的。推荐的自底向上策略:首先将最底层的库模块化,然后逐步向上迁移。在迁移完成前,可以使用--class-path保留非模块化代码,使用--add-exports和--add-reads解决临时依赖。
参考:https://vrhyh.cn/category/yinshi.html
模块化与依赖注入框架:Spring、Guice等框架大量使用反射。模块化要求被反射访问的包必须被opens(或open整个模块)。框架需要适配模块化,或使用--add-opens参数。Spring从5.1版本开始完全支持模块化。
模块化与测试:测试代码通常与被测代码在不同模块。测试模块需要requires被测模块,并使用opens允许测试框架访问私有成员。JUnit5等测试框架需要对应的模块化适配。
模块化的性能影响:模块解析在启动时执行,开销微乎其微。自定义运行时镜像(jlink)可以显著减少启动时间和内存占用。模块化带来的封装性使JIT编译器可以做更多优化(如内联跨越模块边界的调用),但实际效果取决于应用结构。
模块化的工具支持:Maven从3.5开始支持模块化项目,需要配置maven-compiler-plugin启用模块路径。Gradle从4.6开始支持。IDE(IntelliJ、Eclipse)提供模块描述符编辑器和模块图可视化。
模块化的最大受益者是大型应用和框架。对于小型应用,模块化的收益可能不明显,但值得为了未来可维护性而采用。Java平台本身已经模块化,开发者应尽快适配,避免未来升级受阻。
参考:https://oqmyh.cn