← 返回题目列表

Java 9 的模块系统(JPMS)是什么?module-info.java 解决了什么问题?

困难 第 24 / 24 题 更新于 2026/07/28
JPMS模块系统module-infoJigsaw

简化版

Java 9 引入的模块系统(JPMS,Java Platform Module System,项目代号 Jigsaw)是对「Java 代码组织方式」的一次大升级——在「包(package)」之上加了一层「模块(module)」,用 module-info.java 文件显式声明「这个模块依赖哪些模块、对外暴露哪些包」。它解决的核心问题:① 强封装——过去一个 public 类,任何人都能访问(哪怕它是内部实现);模块化后,一个包只有被 exports 导出才能被别的模块访问,没导出的包是真正「模块私有」的(连反射默认也访问不了),实现了真正的封装;② 显式依赖——requires 声明依赖哪些模块,编译/启动时就检查依赖是否齐全(可靠配置,避免运行时才发现缺类的 ClassNotFoundException);③ 拆分庞大的 JDK——把 JDK 拆成几十个模块(java.basejava.sql 等),可以用 jlink 裁剪出「只含用到的模块」的精简运行时(减小体积)。现实:JPMS 很强大但应用层普及度低(大多数业务项目没用 module-info,还是用 classpath),主要是 JDK 自身和一些库在用。

详细版

module-info.java 的核心指令

指令作用
module xxx { }声明一个模块
requires M依赖模块 M
requires transitive M依赖 M 且传递(依赖我的也自动依赖 M)
exports P导出包 P(对外可访问)
exports P to M只对模块 M 导出包 P
opens P开放包 P 给反射(深度反射访问)
uses S / provides S with ISPI 服务的使用/提供
// module-info.java(放在模块根目录)
module com.example.myapp {
    requires java.sql;                    // 依赖 java.sql 模块
    requires transitive com.example.api;  // 传递依赖

    exports com.example.myapp.api;        // 导出这个包(别人能用)
    // com.example.myapp.internal 没 exports → 模块私有,别人访问不了

    opens com.example.myapp.model;        // 开放给反射(如 JSON 序列化框架)

    provides com.example.Service          // 提供 SPI 服务
        with com.example.myapp.ServiceImpl;
}
classpath(传统) vs module-path(模块化):
  classpath:所有 jar 平铺,一个 public 类全世界可见,无强封装
  module-path:模块边界清晰,只有 exports 的包可见,requires 声明依赖

⚠️ JPMS 的核心理念是「强封装 + 显式依赖」,但它「叫好不叫座」——概念先进却在应用层普及率很低。原因:① 迁移成本高——给已有大项目加 module-info 要理清所有依赖和导出,很麻烦;很多老库没模块化,用它们要靠「自动模块(automatic module)」过渡,体验割裂;② 破坏了大量依赖反射「深度访问」的框架——过去框架(Spring、Hibernate、各种序列化库)靠反射访问私有成员,模块化后没 opens 就访问不了(InaccessibleObjectException),要么加 opens、要么加启动参数 --add-opens。所以现实是:JDK 自身彻底模块化了,但绝大多数业务应用还在用 classpath(不写 module-info),享受不到强封装,但也避开了迁移的麻烦。面试知道「JPMS 是什么、解决什么、为什么普及慢」即可。

完整版教学

一、问题:Java 9 前的代码组织缺陷

先理解 JPMS 要解决的历史问题:

Java 9 之前的代码组织:
  类 → 包(package)→ jar → classpath
  一堆 jar 平铺在 classpath 上

三个缺陷:
  ① 没有真正的封装:
     public 类全世界可见——即使是"内部实现"的类,
     只要是 public,任何代码都能访问(拦不住)
     → 内部实现被滥用、无法真正隐藏

  ② 依赖不明确(JAR Hell):
     classpath 上一堆 jar,谁依赖谁不清楚
     缺了某个 jar,编译不报错、运行时才 ClassNotFoundException
     版本冲突、重复类,运行时才炸
     → "JAR 地狱"

  ③ JDK 太庞大、无法裁剪:
     整个 JDK 是一个巨大的 rt.jar
     哪怕只用一点点,也要带整个 JDK(几百 MB)
     → 无法做精简的运行时

  → 这三个问题促成了 JPMS

JPMS 要解决 Java 9 前的三个缺陷:① 没有真正的封装(public 类全世界可见,内部实现类只要 public 就拦不住被访问);② 依赖不明确(JAR Hell)(classpath 一堆 jar 谁依赖谁不清楚,缺 jar 运行时才 ClassNotFoundException、版本冲突运行时才炸);③ JDK 太庞大无法裁剪(整个 rt.jar 几百 MB、用一点也要带全部)。理解「Java 9 前三缺陷:没真正封装(public 全可见)、依赖不明确(JAR Hell 运行时才发现缺类)、JDK 庞大无法裁剪」,就理解了 JPMS 要解决什么。

二、模块:包之上的一层

JPMS 引入「模块」——在包之上加一层:

新的层次:
  类 → 包 → 模块(module)→ ...

模块是什么:
  一组相关的包 + 一个 module-info.java(模块描述符)
  module-info 声明:
    - 这个模块叫什么(module 名)
    - 依赖哪些模块(requires)
    - 对外导出哪些包(exports)

module-info.java(放在模块根目录):
  module com.example.myapp {
      requires java.sql;            // 我依赖 java.sql
      exports com.example.myapp.api; // 我导出这个包
  }

模块边界:
  模块内部的包,只有 exports 的才能被外部访问
  没 exports 的包 = 模块私有(外部看不到、访问不了)
  → 这就是"强封装"

对比包:
  包只是命名空间 + 包级访问控制(同包可见)
  模块是更大的封装单元 + 显式依赖声明

JPMS 引入「模块」——在包之上加一层:模块 = 一组相关的包 + module-info.java(模块描述符)module-info 声明模块名、requires(依赖哪些模块)、exports(导出哪些包)。模块边界:模块内只有 exports 的包能被外部访问、没 exports 的包是模块私有(外部看不到)——这就是强封装。对比:包只是命名空间,模块是更大的封装单元 + 显式依赖声明。理解「模块=一组包+module-info(声明模块名/requires 依赖/exports 导出)、只有 exports 的包外部可见没导出的是模块私有(强封装)、模块是包之上的封装单元」,就理解了模块的概念。

三、强封装:exports 与 opens

「强封装」是 JPMS 的核心价值——exportsopens 控制访问:

exports P:导出包 P(编译期 + 运行期的普通访问)
  别的模块能访问 P 里的 public 类型
  没 exports 的包 → 外部完全访问不了(连 public 类都不行)

  → 实现真正的封装:
    模块作者能明确"哪些是 API(exports)、哪些是内部实现(不 exports)"
    内部实现包不导出 → 外部想用也用不了 → 内部实现可以自由改

exports P to M:定向导出
  只对指定模块 M 导出 P(其他模块还是访问不了)
  → 更精细的控制(只给信任的模块用)

opens P:开放包给反射
  普通 exports 允许"编译期访问 public 成员"
  但反射的"深度访问"(访问 private 成员、setAccessible)需要 opens
  opens P → 允许对 P 里的类型做深度反射(框架序列化/注入常需要)
  没 opens → 反射深度访问抛 InaccessibleObjectException

  opens P to M:定向开放

区别:
  exports:编译 + 运行期的普通(public)访问
  opens:允许运行期的深度反射访问(含 private)
  → 序列化框架(Jackson)、DI 框架(Spring)常需要 opens

强封装靠 exportsopensexports P 导出包 P(别的模块能访问其 public 类型,没 exports 的包外部完全访问不了,实现真正封装——内部实现包不导出就能自由改);exports P to M 定向导出(只给指定模块);opens P 开放包给反射深度访问exports 只允许编译期访问 public 成员,反射访问 private/setAccessible 需要 opens,否则抛 InaccessibleObjectException——序列化/DI 框架常需要)。理解「exports 导出包(没导出外部访问不了=真封装)、exports to 定向导出、opens 开放反射深度访问(exports 只 public、反射访问 private 需 opens 否则异常、框架常需要)」,就掌握了强封装机制。

四、显式依赖:requires

「显式依赖」靠 requires——编译/启动时就检查:

requires M:声明依赖模块 M
  module myapp {
      requires java.sql;   // 我需要 java.sql
  }
  → 编译和启动时,检查 java.sql 模块是否存在
  → 缺了就报错(在启动时,而非运行到某处才 ClassNotFoundException)

好处(可靠配置):
  过去:缺 jar 运行时才炸(ClassNotFoundException)
  模块化:启动时就检查所有 requires 的模块齐全 → 早失败、可靠

requires transitive M:传递依赖
  module api {
      requires transitive com.foundation;
  }
  → 依赖 api 的模块,自动也能访问 com.foundation
  → "我用的 API 暴露了 com.foundation 的类型,用我的人也需要它"
  → 避免使用者还要手动 requires com.foundation

requires static M:编译期依赖(运行期可选)
  → 编译需要,运行时不强制(如可选功能、注解处理)

java.base:所有模块隐式依赖
  java.base 是最核心的模块(Object、String、集合等)
  所有模块自动 requires java.base(不用写)

显式依赖靠 requiresrequires M 声明依赖模块 M,编译/启动时检查模块是否齐全(缺了启动时报错、而非运行到某处才 ClassNotFoundException——可靠配置、早失败)。requires transitive M 传递依赖(依赖我的模块自动也能访问 M,用于「我的 API 暴露了 M 的类型」)。requires static M 编译期依赖(运行可选)。java.base 所有模块隐式依赖(核心模块含 Object/String/集合,不用写)。理解「requires 声明依赖(编译/启动时检查齐全、缺了早失败可靠配置)、requires transitive 传递依赖、java.base 隐式依赖」,就掌握了显式依赖。

JPMS 的一大成果是「JDK 自身模块化」,配合 jlink 裁剪:

JDK 被拆成几十个模块:
  java.base(核心:Object、String、集合、并发...)
  java.sql(JDBC)
  java.xml、java.desktop(Swing/AWT)、java.logging...
  → 原来一个大 rt.jar,拆成模块化的 JDK

好处:
  ① 结构清晰——JDK 内部依赖关系明确
  ② 可裁剪——用 jlink 生成"只含用到的模块"的精简运行时

jlink(模块化带来的能力):
  jlink --add-modules java.base,java.sql,com.myapp
        --output myruntime
  → 生成一个只含这几个模块的自定义 JRE
  → 体积小很多(不带用不到的模块)
  → 适合容器化部署(镜像更小)、嵌入式

对比过去:
  过去要带整个 JDK(几百 MB),哪怕只用一点
  现在 jlink 裁剪 → 几十 MB 的精简运行时

所以 JDK 模块化 + jlink 是 JPMS 的重要实际价值:
  更小的运行时、更适合云原生/容器

JPMS 的一大成果是「JDK 自身模块化」——JDK 被拆成几十个模块(java.base 核心、java.sqljava.xml 等,原来一个大 rt.jar 拆成模块)。好处:结构清晰 + 可裁剪。配合 jlink 能生成「只含用到的模块」的精简运行时(体积小很多,适合容器化/嵌入式——过去要带整个 JDK 几百 MB、现在裁剪到几十 MB)。这是 JPMS 的重要实际价值(更小运行时、适合云原生)。理解「JDK 拆成几十个模块(java.base 核心/java.sql 等)、jlink 生成只含用到模块的精简运行时(体积小适合容器)、是 JPMS 重要实际价值」,就理解了 JDK 模块化和 jlink。

六、现实:为什么普及慢

理解 JPMS 「叫好不叫座」的现实:

JPMS 概念先进,但应用层普及率低,原因:
  ① 迁移成本高:
     给已有项目加 module-info → 要理清所有依赖、导出
     大项目工作量大、容易出错
  ② 生态未跟上:
     很多老库没模块化,用它们要靠"自动模块"(automatic module)
     —— 把非模块化 jar 当一个模块,名字从 jar 名推导,体验割裂
  ③ 破坏了依赖反射的框架:
     Spring、Hibernate、Jackson 等靠反射深度访问
     模块化后没 opens 就访问不了 → InaccessibleObjectException
     要加 opens 或 --add-opens 启动参数 → 麻烦

现实状况:
  - JDK 自身:彻底模块化了(内部实现)
  - 大多数业务应用:不写 module-info,还是用 classpath
    → 享受不到强封装,但也避开了迁移麻烦
  - 一些库/框架:逐步支持模块化(提供 Automatic-Module-Name)

结论:
  JPMS 是好设计,但落地阻力大,应用层普及慢
  面试知道"是什么、解决什么、为什么普及慢"即可
  实际大多数项目还是 classpath

JPMS「叫好不叫座」的原因:① 迁移成本高(给已有项目加 module-info 要理清所有依赖导出、工作量大);② 生态未跟上(很多老库没模块化,用「自动模块」过渡、体验割裂);③ 破坏依赖反射的框架(Spring/Hibernate/Jackson 靠反射深度访问,模块化后没 opens 就 InaccessibleObjectException,要加 opens/--add-opens)。现实:JDK 自身彻底模块化,但大多数业务应用不写 module-info、还用 classpath(享受不到强封装但避开迁移麻烦)。面试知道「是什么、解决什么、为什么普及慢」即可。理解「JPMS 普及慢原因:迁移成本高/生态未跟上(自动模块过渡)/破坏依赖反射的框架(没 opens 就异常);现实 JDK 模块化了但业务应用还用 classpath」,就理解了 JPMS 的现实处境。

记忆钩子:「JPMS(Java 9 模块系统,代号 Jigsaw)=在包之上加模块层,用 module-info.java 声明依赖和导出;解决三问题:①强封装(exports 导出的包才能被外部访问、没导出的是模块私有真封装,opens 开放反射深度访问)②显式依赖(requires 声明依赖、编译/启动时检查齐全早失败=可靠配置、requires transitive 传递依赖、java.base 隐式依赖)③拆分庞大 JDK(拆成几十模块 java.base/java.sql、jlink 裁剪精简运行时适合容器);★叫好不叫座:迁移成本高/生态未跟上(自动模块)/破坏依赖反射框架(没 opens 就 InaccessibleObjectException),JDK 模块化了但业务应用还用 classpath;面试知道是什么解决什么为什么普及慢即可」

七、常见误区与追问

  • 误区:模块就是包的另一种叫法。 不是——包是命名空间 + 包级访问控制;模块是包之上的更大封装单元 + 显式依赖声明(module-info.java),一个模块含多个包,且只有 exports 的包才能被外部访问(强封装),requires 声明模块依赖。
  • 误区:public 类在模块化后还是全世界可见。 不是——模块化后,一个 public 类只有它所在的包被 exports 导出,才能被别的模块访问;没导出的包里的 public 类是「模块私有」的、外部访问不了;这实现了真正的封装(public 不再等于全局可见)。
  • 误区:exports 一个包,反射就能深度访问它。 exports 只允许编译期/运行期访问 public 成员;反射的深度访问(访问 private、setAccessible)需要 opens;没 opens 时反射深度访问会抛 InaccessibleObjectException;序列化框架(Jackson)、DI 框架(Spring)常需要 opens。
  • 误区:所有 Java 项目 Java 9 后都用了模块系统。 大多数业务应用没用——不写 module-info、还是用 classpath;JPMS 迁移成本高、生态未完全跟上、且破坏了很多依赖反射的框架,所以应用层普及率很低;只有 JDK 自身和部分库彻底模块化了。
  • 追问:JPMS 的「强封装」是怎么实现的? 通过 exports——模块里只有被 exports 导出的包,其 public 类型才能被别的模块访问;没导出的包(内部实现)外部完全访问不了(连 public 类都不行、反射也默认不行);这样模块作者能明确区分「API(导出)」和「内部实现(不导出)」,内部实现可以自由修改而不怕被外部依赖。
  • 追问:requires 和 requires transitive 有什么区别? requires M:声明本模块依赖 M(只有本模块能访问 M);requires transitive M:依赖 M 且传递——依赖本模块的其他模块也自动能访问 M;用于「本模块的 API 在方法签名里暴露了 M 的类型」的场景(这样使用者不用再手动 requires M)。
  • 追问:jlink 有什么用?和模块化什么关系? jlink 是模块化带来的能力——它能根据你的应用需要的模块,生成一个「只包含这些模块 + 依赖」的自定义精简运行时(自定义 JRE);因为 JDK 模块化了、依赖关系明确,jlink 才能精确裁剪;生成的运行时体积小很多(几十 MB vs 几百 MB 的完整 JDK),适合容器化部署(镜像更小)、嵌入式设备。

八、加强记忆

Java 9 的模块系统(JPMS,代号 Jigsaw)在「包」之上加了一层「模块」——用 module-info.java 显式声明「依赖哪些模块(requires)、导出哪些包(exports)」。解决三个核心问题:① 强封装——过去 public 类全世界可见,模块化后只有 exports 的包才能被外部访问、没导出的包是模块私有(真正封装,内部实现可自由改);opens 开放包给反射深度访问exports 只允许 public 访问,反射访问 private 需 opens,否则 InaccessibleObjectException,序列化/DI 框架常需要);② 显式依赖——requires 声明依赖,编译/启动时检查模块齐全(早失败、可靠配置,避免运行时 ClassNotFoundException),requires transitive 传递依赖,java.base 隐式依赖;③ 拆分庞大 JDK——JDK 拆成几十个模块(java.base/java.sql),配合 jlink 裁剪出只含用到模块的精简运行时(体积小、适合容器)。现实:JPMS 概念先进但「叫好不叫座」——迁移成本高、生态未跟上(自动模块过渡)、破坏了依赖反射的框架;JDK 自身彻底模块化了,但大多数业务应用还用 classpath(不写 module-info)。面试知道「是什么、解决什么、为什么普及慢」即可。一句话「JPMS(Java9 模块系统)=包之上加模块层用 module-info 声明 requires/exports;解决强封装(只有 exports 的包外部可见、opens 给反射)、显式依赖(requires 编译/启动检查早失败)、拆分 JDK(几十模块+jlink 裁剪精简运行时);叫好不叫座:迁移成本高/破坏依赖反射框架,业务应用还用 classpath」。