Java模块化系统JPMS入门

Java模块化系统JPMS入门

Java 9 引入了 Java Platform Module System(JPMS),这是 Java 语言发展史上的重大变革。本文介绍模块化的核心概念和基本用法。

为什么需要模块化

  1. 更小的运行时:只打包需要的模块,构建精简 JRE
  2. 强封装性:明确控制哪些包可以对外暴露
  3. 可靠的配置:编译期检查模块依赖
  4. 改进安全性:内部实现细节不再暴露

模块基础

module-info.java

每个模块根目录下需要声明模块描述符:

module com.example.app {
// 依赖其他模块
requires java.base; // 默认隐式依赖
requires java.sql;
requires com.example.lib;

// 导出包(其他模块可访问)
exports com.example.app.api;

// 开放包(允许反射访问)
opens com.example.app.entity;

// 提供服务实现
provides com.example.spi.Logger
with com.example.app.FileLogger;

// 消费服务
uses com.example.spi.Logger;
}

核心指令

指令 说明
requires 声明依赖模块
requires transitive 传递依赖
exports 导出包
exports to 定向导出
opens 开放包(反射)
opens to 定向开放
provides with 提供服务实现
uses 消费服务

模块依赖示例

库模块

// com.example.lib/module-info.java
module com.example.lib {
exports com.example.lib.api; // 对外暴露API
exports com.example.lib.dto;

opens com.example.lib.entity; // 允许反射(JPA等框架需要)

requires static java.compiler; // 可选依赖(编译期需要,运行期可选)
}

应用模块

// com.example.app/module-info.java
module com.example.app {
requires com.example.lib;
requires java.logging;

exports com.example.app.controller;

opens com.example.app to spring.core; // 对Spring开放
}

编译与运行

编译模块

# 编译库模块
javac -d out/lib $(find lib -name "*.java")

# 编译应用模块,指定模块路径
javac --module-path out/lib -d out/app $(find app -name "*.java")

运行模块

# 运行主模块
java --module-path out/lib:out/app -m com.example.app/com.example.app.Main

# 查看模块列表
java --list-modules

# 查看模块详情
java --describe-module java.base

打包 JMOD

# 创建模块化的JAR
jar --create --file out/app.jar --main-class com.example.app.Main -C out/app .

# 创建JMOD(用于jlink)
jmod create --class-path out/app out/app.jmod

自定义运行时镜像

使用 jlink 创建精简的 JRE:

jlink --module-path $JAVA_HOME/jmods:out/app \
--add-modules com.example.app \
--launcher app=com.example.app/com.example.app.Main \
--output myapp-runtime

# 运行自定义运行时
./myapp-runtime/bin/app

效果:运行时镜像仅包含必要的模块,体积可从 200MB+ 减少到 50MB 以下。

未命名模块与自动模块

未命名模块(Unnamed Module)

类路径(classpath)上的所有类都属于未命名模块:

  • 可以读取所有模块
  • 所有模块可以读取它
  • 用于兼容性过渡

自动模块(Automatic Module)

将普通 JAR 放在模块路径上,自动成为模块:

  • 模块名从 JAR 文件名或 Automatic-Module-Name 属性推断
  • 导出所有包
  • 依赖所有模块
java --module-path libs:out/app --class-path legacy-lib.jar -m com.example.app

迁移策略

自下而上迁移

  1. 从最底层的库开始模块化
  2. 逐步向上迁移依赖项目
  3. 顶层应用最后迁移

关键步骤

# 1. 使用jdeps分析依赖
jdeps --recursive --class-path libs app.jar

# 2. 生成模块描述符建议
jdeps --generate-module-info out app.jar

# 3. 解决循环依赖
# 通过重构代码或使用服务加载器(ServiceLoader)打破循环

与现有框架集成

Spring Boot

Spring 5+ 支持 JPMS:

module com.example.demo {
requires spring.web;
requires spring.boot;
requires spring.boot.autoconfigure;

opens com.example.demo to spring.core, spring.beans, spring.context;
}

注意事项

  1. 反射访问需要 opens:Hibernate、Jackson 等框架需要反射,需要开放相应包
  2. 资源加载方式变化:使用 Module.getResourceAsStream()
  3. SPI 变化:推荐使用 provides/uses 替代 META-INF/services

总结

场景 建议
新建项目 直接使用模块化
类库开发 添加 module-info.java,支持模块化用户
遗留项目 先保证在模块路径上可用(自动模块)
云原生部署 使用 jlink 构建精简运行时

模块化是 Java 平台现代化的重要一步,虽然迁移有成本,但长期收益显著:更小的部署包、更清晰的依赖关系、更好的封装性。


   转载规则


《Java模块化系统JPMS入门》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录