欢迎光临
我们一直在努力

你还在忍受“JAR 地狱”吗?都 Java 9+ 了,为什么不敢用 JPMS 把它一刀切了?

👋 你好,欢迎来到我的博客!我是【菜鸟不学编程】    我是一个正在奋斗中的职场码农,步入职场多年,正在从“小码农”慢慢成长为有深度、有思考的技术人。在这条不断进阶的路上,我决定记录下自己的学习与成长过程,也希望通过博客结识更多志同道合的朋友。      🛠️ 主要方向包括 Java 基础、Spring 全家桶、数据库优化、项目实战等,也会分享一些踩坑经历与面试复盘,希望能为还在迷茫中的你提供一些参考。   💡 我相信:写作是一种思考的过程,分享是一种进步的方式。       如果你和我一样热爱技术、热爱成长,欢迎关注我,一起交流进步!

全文目录:

    • I. JPMS 概述:它到底解决了什么,为什么我说“早该这样”?
    • II. `module-info.java`:模块宣言书,写得好能少掉一半锅
      • 1)`module`:模块名怎么取才不挨骂?
      • 2)`exports`:我允许别人用哪些包(对外开放的边界)
      • 3)`requires`:我依赖谁(并且依赖必须可解析)
      • 4)`uses`:我要消费一个服务(配合 `ServiceLoader`)
      • 5)`provides … with …`:我提供一个服务实现
    • III. 模块路径 vs 类路径:`–module-path` 不是摆设,它是“分界线”
      • 1)classpath(传统方式)
      • 2)module-path(JPMS 方式)
      • 3)混用场景(现实里非常常见)
    • IV. 服务提供与消费:`provides` + `uses` 才是 JPMS 的精华戏份
    • V. 迁移旧项目:从 classpath 到模块化,我建议你按这个顺序走(少挨打)
      • Step 1:先盘点依赖(别闭眼改)
      • Step 2:先做“模块边界切分”
      • Step 3:让旧 jar 先“自动模块化”(Automatic Module)
      • Step 4:处理“反射/深访问”问题(常见在框架里)
      • Step 5:最后再做“强封装收紧”
    • VI. 示例:构建一个模块化的多层应用(带服务提供与消费,能跑的那种)
      • 1)service-api 模块
      • 2)service-impl 模块(提供服务)
      • 3)app 模块(消费服务)
      • 4)编译与运行(重点来了 ✅)
        • 编译 service-api
        • 编译 service-impl(需要 module-path 指向 api)
        • 编译 app
        • 运行(用 module-path + -m)
    • 额外加餐:我踩过的几个“JPMS 真实世界坑位”(提前给你避雷 ⚠️)
      • 1)“为什么我明明 `public` 了,别的模块却看不到?”
      • 2)“第三方库模块名怎么这么丑?”
      • 3)“框架反射爆炸怎么办?”
      • 4)“模块化后,打包部署会不会更麻烦?”
    • 收个尾: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-exports
    • open 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)
    未经允许不得转载:171主机测评 » 你还在忍受“JAR 地狱”吗?都 Java 9+ 了,为什么不敢用 JPMS 把它一刀切了?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址