JPMS:Java 平台模块系统(Java Platform Module System)

提示:目前maven上的库,大部分不支持JPMS

介绍

从 Java 9 正式引入,Java Platform Module System,缩写:JPMS,也叫 Jigsaw 项目

目标

把庞大的 JDK 拆成模块、强封装、显式依赖、控制访问边界,解决传统JAR包“类路径地狱(classpath hell)”、包随意访问、无法声明依赖等问题。

传统ClassPath的痛点

  1. 所有类都在扁平的classpath,没有版本隔离,多个版本jar冲突直接覆盖
  2. 包访问权限不可控:只要在classpath,就能反射访问其他包非 public 类
  3. 无法显式声明依赖,运行时报类缺失,编译期不知道需要哪些jar
  4. JDK本身是一个巨大整体,哪怕只写简单程序也要加载整个 rt.jar

例子

  1. 所有类都在扁平classpath,没有版本隔离,多版本jar冲突直接覆盖
    👉 业务/第三方jar(程序员项目里引入的jar包)
    classpath是一个扁平列表,例如:
    lib/guava-18.jar, lib/guava-21.jar
    两个版本同时放进去,类加载顺序决定哪个生效,另一个版本的类直接被“掩盖”,也就是经典Jar Hell。

  2. 包访问权限不可控:只要在classpath,就能反射访问其他包非public类
    👉 业务jar、第三方jar、JDK rt.jar,全部都有这个问题
    只要类在classpath,就算是包私有、private类,代码都可以通过反射强行访问。

    • 业务库:可以直接反射调用第三方库内部实现类(破坏封装,升级容易崩)
    • JDK:可以反射访问sun.misc.Unsafe这类JDK内部API。Java9就是靠模块化把JDK内部包封闭起来,禁止外部模块访问。
  3. 无法显式声明依赖,运行时报类缺失,编译期不知道需要哪些jar
    👉 程序员应用项目(业务+第三方jar)
    传统Maven/Gradle是构建工具层面的依赖,不是Java语言层面的依赖。

    • Maven只是下载jar;
    • JVM本身完全不知道A.jar依赖B.jar;
      你丢少一个jar,编译可能过(IDE带依赖),打包运行才抛ClassNotFoundException。

      JDK内部类之间的依赖是固定写死在rt.jar里,不存在这个痛点。

  4. JDK本身是一个巨大整体,哪怕简单程序也要加载整个rt.jar
    👉 纯指JDK自带类库(Java8及更早)
    Java8及以前,全部JDK核心类都打包在rt.jar,哪怕你只写一行System.out.println(),JDK也带着整个rt.jar。JPMS把rt.jar拆成几十个小JDK模块(java.base、java.sql等),支持jlink裁剪,只打包需要的模块。

解决

JPMS引入模块(Module),每个模块有独立的名字、依赖列表、导出包,实现强封装+依赖声明。

JPMS的设计思路:

  1. 先把JDK拆成模块,解决JDK单体庞大、内部API随意被反射调用;
  2. 同时开放模块能力给应用开发者,让我们自己写的代码、打包的jar,也能用上模块:显式依赖、包导出、强封装。

概念

1. 模块 module

模块是一组包的集合,一个模块 = 多个package + 模块描述文件module-info.java。

注意: 一个jar包可以是一个模块(模块jar),也可以是普通jar(类路径jar,在模块化体系里叫未命名模块 unnamed module)

2. module-info.java(模块描述符,核心文件)

放在源码根目录,和包目录同级,必须在源码根下,不能放到package里面。

// 声明模块名称
module com.example.myapp {
    // 1. 依赖其他模块
    requires java.sql;
    requires transitive com.example.common; // 传递依赖

    // 2. 导出包:允许外部模块访问这个包下public类型
    exports com.example.myapp.service;

    // 3. 只导出给指定模块(限定导出,最严格)
    exports com.example.myapp.internal.util to com.example.web;

    // 4. 开放包:允许反射访问(框架Spring/Hibernate常用)
    opens com.example.myapp.entity;
    opens com.example.myapp.controller to com.example.web;

    // 5. 提供服务:SPI,本模块实现某个服务接口
    provides com.example.spi.DemoService with com.example.myapp.impl.DemoServiceImpl;

    // 6. 使用服务:消费SPI接口
    uses com.example.spi.DemoService;
}

解释

  1. module:定义模块,模块名推荐反向域名,不能和包名强制一致,但建议保持一致
  2. requires:声明编译期+运行期依赖另一个模块
    • requires transitive:传递依赖;A requires transitive B → 依赖A的模块自动获得B,不用再写requires B
  3. exports <包名>:对外暴露public类、接口;不导出的包,外部模块完全不可见(编译报错),即使是public类
  4. exports … to 模块列表:限定导出,只允许指定模块访问该包
  5. opens <包名>:允许反射访问包里面所有类(包括非public),编译期仍然不能直接访问源码;ORM、IOC框架必备
  6. opens … to:限定开放,仅指定模块可反射访问
  7. provides / uses:JPMS内置SPI服务机制,替代旧的META-INF/services

✅ JPMS核心封装规则:
模块内不export的包,其他模块连编译都无法访问;反射也不行,除非opens。
对比旧版本:只要类在classpath,就能反射访问任意包public类。

模块分类

  1. 系统模块:JDK自带模块,java.base是基础核心模块,所有模块隐式依赖java.base,不需要写requires

    java.base        基础:Object, String,集合,IO
    java.sql         JDBC
    java.xml         XML解析
    java.net.http    HttpClient
    jdk.httpserver   内置http服务器
    
  2. 应用模块:自己写的业务模块

  3. 自动模块 Automatic Module
    普通第三方jar(没有module-info.java)放到module-path下,会被自动当成模块:

    • 模块名默认从jar文件名推断(去掉版本号)
    • 自动模块隐式导出所有包(兼容旧代码)

      很多老框架(Spring5、MyBatis早期版本)作为自动模块使用

  4. 未命名模块 Unnamed Module
    jar放在传统-cp / -classpath,全部归到未命名模块。
    规则:

    • 未命名模块可以读取所有模块
    • 命名模块不能requires未命名模块(单向隔离,这是模块化重要限制)

开发建议:新项目用--module-path;老项目迁移时,第三方jar一般作为自动模块。

模块化的限制与坑点

  1. 包冲突:同一个包名不能出现在同一个模块路径下的多个模块中(命名模块不允许包分裂split package)

    传统classpath允许多个jar有相同包;模块化命名模块禁止,自动模块有部分宽松规则

  2. 未命名模块单向访问:命名模块不能requires未命名模块;未命名模块能读取所有命名模块。

  3. java.base不能修改,不能requires其他系统基础模块。
  4. 很多反射框架需要opens,否则会报IllegalAccessException,Spring、Hibernate必须配置opens。
  5. 模块名不能带版本号;版本号放在jar文件名,不在module-info里定义。
  6. 不破坏向下兼容:你可以完全不写module-info.java,继续用classpath老方式,模块化是可选特性。

适用场景 & 不适用场景

适合:

  • 大型多模块项目,组件拆分、强边界管控
  • 做SDK、公共组件库,控制内部类不被外部调用
  • 精简JDK(jlink打包,只保留需要模块,生成最小JRE)

不推荐:

  • 简单小Demo、单jar小工具,增加复杂度
  • 大量老旧第三方库,需要大量opens配置(SpringBoot项目默认不启用JPMS,SpringBoot用classpath为主)