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

全文目录:

I. JPMS 概述:它到底解决了什么,为什么我说“早该这样”?

先说人话:JPMS(Java Platform Module System)就是 Java 9 引入的一套“官方模块化方案”。它最想解决的两个痛点,几乎每个写过 Java 项目的人都踩过坑:

  1. JAR 地狱(JAR Hell)
    依赖冲突、重复类、版本漂移、类加载顺序玄学……你以为是运气问题,其实是机制问题。
    典型场景:你项目里有 guava-20,某个依赖又带了 guava-31,最后跑起来到底用哪个?——看缘分。😇

  2. 强封装与可靠配置(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
  • 不要乱起短名:corecommonutil(后期必撞车)
  • 建议把“层级意图”写出来:...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
🧵 本文原创,转载请注明出处。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐