你还在忍受“JAR 地狱”吗?都 Java 9+ 了,为什么不敢用 JPMS 把它一刀切了?
👋 你好,欢迎来到我的博客!我是【菜鸟不学编程】 我是一个正在奋斗中的职场码农,步入职场多年,正在从“小码农”慢慢成长为有深度、有思考的技术人。在这条不断进阶的路上,我决定记录下自己的学习与成长过程,也希望通过博客结识更多志同道合的朋友。 🛠️ 主要方向包括 Java 基础、Spring 全家桶、数据库优化、项目实战等,也会分享一些踩坑经历与面试复盘,希望能为还在迷茫中的你提供一些参
👋 你好,欢迎来到我的博客!我是【菜鸟不学编程】
我是一个正在奋斗中的职场码农,步入职场多年,正在从“小码农”慢慢成长为有深度、有思考的技术人。在这条不断进阶的路上,我决定记录下自己的学习与成长过程,也希望通过博客结识更多志同道合的朋友。
🛠️ 主要方向包括 Java 基础、Spring 全家桶、数据库优化、项目实战等,也会分享一些踩坑经历与面试复盘,希望能为还在迷茫中的你提供一些参考。
💡 我相信:写作是一种思考的过程,分享是一种进步的方式。
如果你和我一样热爱技术、热爱成长,欢迎关注我,一起交流进步!
全文目录:
-
- I. JPMS 概述:它到底解决了什么,为什么我说“早该这样”?
- II. `module-info.java`:模块宣言书,写得好能少掉一半锅
- III. 模块路径 vs 类路径:`--module-path` 不是摆设,它是“分界线”
- IV. 服务提供与消费:`provides` + `uses` 才是 JPMS 的精华戏份
- V. 迁移旧项目:从 classpath 到模块化,我建议你按这个顺序走(少挨打)
- VI. 示例:构建一个模块化的多层应用(带服务提供与消费,能跑的那种)
- 额外加餐:我踩过的几个“JPMS 真实世界坑位”(提前给你避雷 ⚠️)
- 收个尾:JPMS 值不值得学?我反问你一句——你还想被依赖冲突按在地上摩擦多久?
- 📝 写在最后
I. JPMS 概述:它到底解决了什么,为什么我说“早该这样”?
先说人话:JPMS(Java Platform Module System)就是 Java 9 引入的一套“官方模块化方案”。它最想解决的两个痛点,几乎每个写过 Java 项目的人都踩过坑:
-
JAR 地狱(JAR Hell)
依赖冲突、重复类、版本漂移、类加载顺序玄学……你以为是运气问题,其实是机制问题。
典型场景:你项目里有guava-20,某个依赖又带了guava-31,最后跑起来到底用哪个?——看缘分。😇 -
强封装与可靠配置(Strong Encapsulation / Reliable Configuration)
以前 classpath 是“开放世界”:- 你想访问别人 jar 里的内部包?随便。
- 你想反射搞点小动作?也行。
- 你想把依赖漏写?也能跑,直到线上炸。
JPMS 的态度就一个字:不。
它把依赖关系从“运行时碰运气”变成“编译/启动时就检查”。你的项目会更“烦”,但也更“稳”。
一句话总结:
classpath 是江湖,JPMS 是秩序。江湖很爽,但秩序能保命。🙂
II. module-info.java:模块宣言书,写得好能少掉一半锅
模块化的核心文件就是 module-info.java。它一般长这样:
module com.example.app {
requires com.example.service.api;
uses com.example.service.api.GreetingService;
}
1)module:模块名怎么取才不挨骂?
- 通常用反向域名风格:
com.company.project.feature - 不要乱起短名:
core、common、util(后期必撞车) - 建议把“层级意图”写出来:
...api、...impl、...app
2)exports:我允许别人用哪些包(对外开放的边界)
module com.example.service.api {
exports com.example.service.api;
}
exports 的意思是:只有被 exports 的包,别的模块才“看得见”。
没 exports?对外就是不可见(哪怕类是 public)。
你以为 public 就是公共?在 JPMS 里:
public只是“模块内部公共”,exports才是“模块外部公共”。
这句我当初没懂,踩了个结结实实的坑。🙃
3)requires:我依赖谁(并且依赖必须可解析)
requires java.sql;
requires com.example.service.api;
常见变体:
requires transitive xxx:把依赖“传递”给依赖我的模块(API 模块里很常用)requires static xxx:编译期需要,运行期可选(比如注解处理器相关)
4)uses:我要消费一个服务(配合 ServiceLoader)
uses com.example.service.api.GreetingService;
5)provides ... with ...:我提供一个服务实现
provides com.example.service.api.GreetingService
with com.example.service.impl.SimpleGreetingService;
III. 模块路径 vs 类路径:--module-path 不是摆设,它是“分界线”
很多人第一次跑模块化应用,会迷糊:
“我到底该用 classpath 还是 module-path?”
1)classpath(传统方式)
java -cp libs/*:out com.example.Main
特点:
- 依赖可见性全开(只要在 cp 上就能看到)
- 冲突更隐蔽(顺序决定胜负)
- 适合旧项目,但“长期债务”很重
2)module-path(JPMS 方式)
java --module-path mods -m com.example.app/com.example.app.Main
这里的 -m 是重点:
-m 模块名/主类:告诉 JVM 用模块方式启动--module-path:只放“模块化 jar”或“模块输出目录”
更关键的区别:
放在 module-path 上的东西,会被当成“模块世界”的居民,受 exports/requires 约束。
放在 classpath 上的东西,会进入“Unnamed Module(未命名模块)”,基本就是“老江湖”。
3)混用场景(现实里非常常见)
迁移期你可能会这样启动:
- 你自己的代码走模块系统
- 第三方库还是放 classpath(或自动模块)
这很正常,别一上来就追求“纯血模块化”,那属于自找苦吃 😂。
IV. 服务提供与消费:provides + uses 才是 JPMS 的精华戏份
说个真实感受:JPMS 里最让我眼前一亮的不是封装,而是服务机制变得“名正言顺”。
你以前可能也用过 ServiceLoader,但配置文件在 META-INF/services/...,很容易散、很容易忘。
JPMS 直接把这套机制写进 module-info.java:声明式、可检查、可读性高。
V. 迁移旧项目:从 classpath 到模块化,我建议你按这个顺序走(少挨打)
我不鼓励“一次性模块化重构”。真的,别冲动。😅
更稳的路线是:先让它跑,再让它更模块化。
Step 1:先盘点依赖(别闭眼改)
- 用
jdeps看依赖关系(JDK 自带)
jdeps --multi-release 17 --print-module-deps target/yourapp.jar
Step 2:先做“模块边界切分”
通常从这三块切最稳:
...api:对外接口(exports)...impl:实现(不 exports 或少 exports)...app:启动入口(main)
Step 3:让旧 jar 先“自动模块化”(Automatic Module)
如果第三方 jar 没有 module-info.class,放到 module-path 上时它会变成自动模块。
模块名通常来自:
- jar 的
Automatic-Module-Name(最理想) - 否则由 jar 文件名推导(可能很丑)
提醒一句:自动模块是过渡方案,不是终点。它基本“全 exports”,封装性不强,但能让你逐步迁移。
Step 4:处理“反射/深访问”问题(常见在框架里)
如果你用到反射(尤其是 Spring、Jackson、Hibernate),可能会遇到:
InaccessibleObjectException- 深层包不可见
迁移期常用手段(谨慎使用):
--add-opens--add-exportsopen module ... { }(开放整个模块反射访问)
能不用就别用,因为一用就容易回到“江湖模式”。🙃
Step 5:最后再做“强封装收紧”
先把模块跑通,再逐步减少 exports,收紧边界。
不然你会在“启动不了”和“缺这个缺那个”之间反复横跳,心态很难不爆炸。😵💫
VI. 示例:构建一个模块化的多层应用(带服务提供与消费,能跑的那种)
下面我们做一个很典型、很“企业味儿”的结构:
service.api:定义服务接口service.impl:提供实现(provides)app:消费服务(uses + ServiceLoader)
目录结构(示意):
jpms-demo/
service-api/
src/main/java/...
service-impl/
src/main/java/...
app/
src/main/java/...
为了让你不被构建工具绑架,我先用 纯 javac/jar 演示(够直观),你用 Maven/Gradle 迁移也更容易理解。
1)service-api 模块
service-api/src/main/java/module-info.java
module com.example.service.api {
exports com.example.service.api;
}
service-api/src/main/java/com/example/service/api/GreetingService.java
package com.example.service.api;
public interface GreetingService {
String greet(String name);
}
2)service-impl 模块(提供服务)
service-impl/src/main/java/module-info.java
module com.example.service.impl {
requires com.example.service.api;
provides com.example.service.api.GreetingService
with com.example.service.impl.SimpleGreetingService;
}
service-impl/src/main/java/com/example/service/impl/SimpleGreetingService.java
package com.example.service.impl;
import com.example.service.api.GreetingService;
public class SimpleGreetingService implements GreetingService {
@Override
public String greet(String name) {
// 带点情绪:程序员的温柔通常只写在字符串里 😂
return "你好呀," + name + "~今天也要少写 Bug 哦!";
}
}
3)app 模块(消费服务)
app/src/main/java/module-info.java
module com.example.app {
requires com.example.service.api;
uses com.example.service.api.GreetingService;
}
app/src/main/java/com/example/app/Main.java
package com.example.app;
import com.example.service.api.GreetingService;
import java.util.ServiceLoader;
public class Main {
public static void main(String[] args) {
String name = args.length > 0 ? args[0] : "陌生人";
ServiceLoader<GreetingService> loader = ServiceLoader.load(GreetingService.class);
GreetingService service = loader.findFirst()
.orElseThrow(() -> new IllegalStateException("没找到 GreetingService 实现,你是不是忘了把 impl 放到 module-path?"));
System.out.println(service.greet(name));
}
}
4)编译与运行(重点来了 ✅)
假设我们在 jpms-demo/ 根目录:
编译 service-api
mkdir -p mods/com.example.service.api
javac -d mods/com.example.service.api \
service-api/src/main/java/module-info.java \
service-api/src/main/java/com/example/service/api/GreetingService.java
编译 service-impl(需要 module-path 指向 api)
mkdir -p mods/com.example.service.impl
javac --module-path mods -d mods/com.example.service.impl \
service-impl/src/main/java/module-info.java \
service-impl/src/main/java/com/example/service/impl/SimpleGreetingService.java
编译 app
mkdir -p mods/com.example.app
javac --module-path mods -d mods/com.example.app \
app/src/main/java/module-info.java \
app/src/main/java/com/example/app/Main.java
运行(用 module-path + -m)
java --module-path mods \
-m com.example.app/com.example.app.Main Britney
你应该会看到类似输出:
你好呀,Britney~今天也要少写 Bug 哦!
如果你没看到这句,而是看到一堆“找不到模块/找不到服务实现”,别慌——这恰恰说明模块系统在认真工作:它在逼你把依赖关系讲清楚。🙂
额外加餐:我踩过的几个“JPMS 真实世界坑位”(提前给你避雷 ⚠️)
1)“为什么我明明 public 了,别的模块却看不到?”
因为你没 exports。
public 不是许可证,exports 才是通行证。
2)“第三方库模块名怎么这么丑?”
因为它可能是自动模块,模块名从 jar 文件名推导出来的。
解决方案:优先选带 Automatic-Module-Name 的版本,或换支持 JPMS 的库版本。
3)“框架反射爆炸怎么办?”
迁移期可以 --add-opens 顶一顶,但最好长期还是梳理边界:
- 哪些包确实需要反射
- 哪些其实是你不该暴露的内部实现
4)“模块化后,打包部署会不会更麻烦?”
短期会。
但长期你会发现:
- 依赖关系更可控
- 启动问题更早暴露
- 安全边界更清晰
尤其是多人协作项目,收益会越来越明显。
收个尾:JPMS 值不值得学?我反问你一句——你还想被依赖冲突按在地上摩擦多久?
说真的,JPMS 的学习曲线不算“爽”,它更像健身:
刚开始你会觉得累、觉得麻烦、觉得“我以前那套挺好”。
但当你项目复杂度上来,模块边界帮你挡掉一堆莫名其妙的问题时,你会突然觉得:
“原来所谓的高级感,就是少背锅。” 😌
📝 写在最后
如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!
我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!
感谢你的阅读,我们下篇文章再见~👋
✍️ 作者:某个被流“治愈”过的 Java 老兵
📅 日期:2026-01-07
🧵 本文原创,转载请注明出处。
更多推荐
所有评论(0)