提示:目前maven上的库,大部分不支持JPMS
介绍
从 Java 9 正式引入,Java Platform Module System,缩写:JPMS,也叫 Jigsaw 项目
目标
把庞大的 JDK 拆成模块、强封装、显式依赖、控制访问边界,解决传统JAR包“类路径地狱(classpath hell)”、包随意访问、无法声明依赖等问题。
传统ClassPath的痛点
- 所有类都在扁平的classpath,没有版本隔离,多个版本jar冲突直接覆盖
- 包访问权限不可控:只要在classpath,就能反射访问其他包非
public类 - 无法显式声明依赖,运行时报类缺失,编译期不知道需要哪些jar
- JDK本身是一个巨大整体,哪怕只写简单程序也要加载整个
rt.jar
例子
所有类都在扁平classpath,没有版本隔离,多版本jar冲突直接覆盖
👉 业务/第三方jar(程序员项目里引入的jar包)
classpath是一个扁平列表,例如:lib/guava-18.jar, lib/guava-21.jar
两个版本同时放进去,类加载顺序决定哪个生效,另一个版本的类直接被“掩盖”,也就是经典Jar Hell。包访问权限不可控:只要在classpath,就能反射访问其他包非public类
👉 业务jar、第三方jar、JDK rt.jar,全部都有这个问题
只要类在classpath,就算是包私有、private类,代码都可以通过反射强行访问。- 业务库:可以直接反射调用第三方库内部实现类(破坏封装,升级容易崩)
- JDK:可以反射访问
sun.misc.Unsafe这类JDK内部API。Java9就是靠模块化把JDK内部包封闭起来,禁止外部模块访问。
无法显式声明依赖,运行时报类缺失,编译期不知道需要哪些jar
👉 程序员应用项目(业务+第三方jar)
传统Maven/Gradle是构建工具层面的依赖,不是Java语言层面的依赖。- Maven只是下载jar;
- JVM本身完全不知道A.jar依赖B.jar;
你丢少一个jar,编译可能过(IDE带依赖),打包运行才抛ClassNotFoundException。JDK内部类之间的依赖是固定写死在rt.jar里,不存在这个痛点。
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的设计思路:
- 先把JDK拆成模块,解决JDK单体庞大、内部API随意被反射调用;
- 同时开放模块能力给应用开发者,让我们自己写的代码、打包的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;
}
解释
- module:定义模块,模块名推荐反向域名,不能和包名强制一致,但建议保持一致
- requires:声明编译期+运行期依赖另一个模块
requires transitive:传递依赖;A requires transitive B → 依赖A的模块自动获得B,不用再写requires B
- exports <包名>:对外暴露public类、接口;不导出的包,外部模块完全不可见(编译报错),即使是public类
- exports … to 模块列表:限定导出,只允许指定模块访问该包
- opens <包名>:允许反射访问包里面所有类(包括非public),编译期仍然不能直接访问源码;ORM、IOC框架必备
- opens … to:限定开放,仅指定模块可反射访问
- provides / uses:JPMS内置SPI服务机制,替代旧的
META-INF/services
✅ JPMS核心封装规则:
模块内不export的包,其他模块连编译都无法访问;反射也不行,除非opens。
对比旧版本:只要类在classpath,就能反射访问任意包public类。
模块分类
系统模块:JDK自带模块,
java.base是基础核心模块,所有模块隐式依赖java.base,不需要写requiresjava.base 基础:Object, String,集合,IO java.sql JDBC java.xml XML解析 java.net.http HttpClient jdk.httpserver 内置http服务器应用模块:自己写的业务模块
自动模块 Automatic Module
普通第三方jar(没有module-info.java)放到module-path下,会被自动当成模块:- 模块名默认从jar文件名推断(去掉版本号)
- 自动模块隐式导出所有包(兼容旧代码)
很多老框架(Spring5、MyBatis早期版本)作为自动模块使用
未命名模块 Unnamed Module
jar放在传统-cp / -classpath,全部归到未命名模块。
规则:- 未命名模块可以读取所有模块
- 命名模块不能requires未命名模块(单向隔离,这是模块化重要限制)
开发建议:新项目用--module-path;老项目迁移时,第三方jar一般作为自动模块。
模块化的限制与坑点
包冲突:同一个包名不能出现在同一个模块路径下的多个模块中(命名模块不允许包分裂split package)
传统classpath允许多个jar有相同包;模块化命名模块禁止,自动模块有部分宽松规则
未命名模块单向访问:命名模块不能requires未命名模块;未命名模块能读取所有命名模块。
java.base不能修改,不能requires其他系统基础模块。- 很多反射框架需要
opens,否则会报IllegalAccessException,Spring、Hibernate必须配置opens。 - 模块名不能带版本号;版本号放在jar文件名,不在module-info里定义。
- 不破坏向下兼容:你可以完全不写module-info.java,继续用classpath老方式,模块化是可选特性。
适用场景 & 不适用场景
适合:
- 大型多模块项目,组件拆分、强边界管控
- 做SDK、公共组件库,控制内部类不被外部调用
- 精简JDK(jlink打包,只保留需要模块,生成最小JRE)
不推荐:
- 简单小Demo、单jar小工具,增加复杂度
- 大量老旧第三方库,需要大量opens配置(SpringBoot项目默认不启用JPMS,SpringBoot用classpath为主)