
你好欢迎来到我的博客我是【菜鸟不学编程】我是一个正在奋斗中的职场码农步入职场多年正在从“小码农”慢慢成长为有深度、有思考的技术人。在这条不断进阶的路上我决定记录下自己的学习与成长过程也希望通过博客结识更多志同道合的朋友。️ 主要方向包括 Java 基础、Spring 全家桶、数据库优化、项目实战等也会分享一些踩坑经历与面试复盘希望能为还在迷茫中的你提供一些参考。 我相信写作是一种思考的过程分享是一种进步的方式。如果你和我一样热爱技术、热爱成长欢迎关注我一起交流进步全文目录I. JPMS 概述它到底解决了什么为什么我说“早该这样”II. module-info.java模块宣言书写得好能少掉一半锅1module模块名怎么取才不挨骂2exports我允许别人用哪些包对外开放的边界3requires我依赖谁并且依赖必须可解析4uses我要消费一个服务配合 ServiceLoader5provides ... with ...我提供一个服务实现III. 模块路径 vs 类路径--module-path 不是摆设它是“分界线”1classpath传统方式2module-pathJPMS 方式3混用场景现实里非常常见IV. 服务提供与消费provides uses 才是 JPMS 的精华戏份V. 迁移旧项目从 classpath 到模块化我建议你按这个顺序走少挨打Step 1先盘点依赖别闭眼改Step 2先做“模块边界切分”Step 3让旧 jar 先“自动模块化”Automatic ModuleStep 4处理“反射/深访问”问题常见在框架里Step 5最后再做“强封装收紧”VI. 示例构建一个模块化的多层应用带服务提供与消费能跑的那种1service-api 模块2service-impl 模块提供服务3app 模块消费服务4编译与运行重点来了 ✅编译 service-api编译 service-impl需要 module-path 指向 api编译 app运行用 module-path -m额外加餐我踩过的几个“JPMS 真实世界坑位”提前给你避雷 ⚠️1“为什么我明明 public 了别的模块却看不到”2“第三方库模块名怎么这么丑”3“框架反射爆炸怎么办”4“模块化后打包部署会不会更麻烦”收个尾JPMS 值不值得学我反问你一句——你还想被依赖冲突按在地上摩擦多久 写在最后I. JPMS 概述它到底解决了什么为什么我说“早该这样”先说人话JPMSJava 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。它一般长这样modulecom.example.app{requirescom.example.service.api;usescom.example.service.api.GreetingService;}1module模块名怎么取才不挨骂通常用反向域名风格com.company.project.feature不要乱起短名core、common、util后期必撞车建议把“层级意图”写出来...api、...impl、...app2exports我允许别人用哪些包对外开放的边界modulecom.example.service.api{exportscom.example.service.api;}exports的意思是只有被 exports 的包别的模块才“看得见”。没 exports对外就是不可见哪怕类是 public。你以为public就是公共在 JPMS 里public只是“模块内部公共”exports才是“模块外部公共”。这句我当初没懂踩了个结结实实的坑。3requires我依赖谁并且依赖必须可解析requiresjava.sql;requirescom.example.service.api;常见变体requires transitive xxx把依赖“传递”给依赖我的模块API 模块里很常用requires static xxx编译期需要运行期可选比如注解处理器相关4uses我要消费一个服务配合ServiceLoaderusescom.example.service.api.GreetingService;5provides ... with ...我提供一个服务实现providescom.example.service.api.GreetingServicewithcom.example.service.impl.SimpleGreetingService;III. 模块路径 vs 类路径--module-path不是摆设它是“分界线”很多人第一次跑模块化应用会迷糊“我到底该用 classpath 还是 module-path”1classpath传统方式java-cplibs/*:out com.example.Main特点依赖可见性全开只要在 cp 上就能看到冲突更隐蔽顺序决定胜负适合旧项目但“长期债务”很重2module-pathJPMS 方式java--module-path mods-mcom.example.app/com.example.app.Main这里的-m是重点-m 模块名/主类告诉 JVM 用模块方式启动--module-path只放“模块化 jar”或“模块输出目录”更关键的区别放在 module-path 上的东西会被当成“模块世界”的居民受 exports/requires 约束。放在 classpath 上的东西会进入“Unnamed Module未命名模块”基本就是“老江湖”。3混用场景现实里非常常见迁移期你可能会这样启动你自己的代码走模块系统第三方库还是放 classpath或自动模块这很正常别一上来就追求“纯血模块化”那属于自找苦吃 。IV. 服务提供与消费providesuses才是 JPMS 的精华戏份说个真实感受JPMS 里最让我眼前一亮的不是封装而是服务机制变得“名正言顺”。你以前可能也用过ServiceLoader但配置文件在META-INF/services/...很容易散、很容易忘。JPMS 直接把这套机制写进module-info.java声明式、可检查、可读性高。V. 迁移旧项目从 classpath 到模块化我建议你按这个顺序走少挨打我不鼓励“一次性模块化重构”。真的别冲动。更稳的路线是先让它跑再让它更模块化。Step 1先盘点依赖别闭眼改用jdeps看依赖关系JDK 自带jdeps --multi-release17--print-module-deps target/yourapp.jarStep 2先做“模块边界切分”通常从这三块切最稳...api对外接口exports...impl实现不 exports 或少 exports...app启动入口mainStep 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提供实现providesapp消费服务uses ServiceLoader目录结构示意jpms-demo/ service-api/ src/main/java/... service-impl/ src/main/java/... app/ src/main/java/...为了让你不被构建工具绑架我先用纯 javac/jar演示够直观你用 Maven/Gradle 迁移也更容易理解。1service-api 模块service-api/src/main/java/module-info.javamodulecom.example.service.api{exportscom.example.service.api;}service-api/src/main/java/com/example/service/api/GreetingService.javapackagecom.example.service.api;publicinterfaceGreetingService{Stringgreet(Stringname);}2service-impl 模块提供服务service-impl/src/main/java/module-info.javamodulecom.example.service.impl{requirescom.example.service.api;providescom.example.service.api.GreetingServicewithcom.example.service.impl.SimpleGreetingService;}service-impl/src/main/java/com/example/service/impl/SimpleGreetingService.javapackagecom.example.service.impl;importcom.example.service.api.GreetingService;publicclassSimpleGreetingServiceimplementsGreetingService{OverridepublicStringgreet(Stringname){// 带点情绪程序员的温柔通常只写在字符串里 return你好呀name今天也要少写 Bug 哦;}}3app 模块消费服务app/src/main/java/module-info.javamodulecom.example.app{requirescom.example.service.api;usescom.example.service.api.GreetingService;}app/src/main/java/com/example/app/Main.javapackagecom.example.app;importcom.example.service.api.GreetingService;importjava.util.ServiceLoader;publicclassMain{publicstaticvoidmain(String[]args){Stringnameargs.length0?args[0]:陌生人;ServiceLoaderGreetingServiceloaderServiceLoader.load(GreetingService.class);GreetingServiceserviceloader.findFirst().orElseThrow(()-newIllegalStateException(没找到 GreetingService 实现你是不是忘了把 impl 放到 module-path));System.out.println(service.greet(name));}}4编译与运行重点来了 ✅假设我们在jpms-demo/根目录编译 service-apimkdir-pmods/com.example.service.api javac-dmods/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 指向 apimkdir-pmods/com.example.service.impl javac --module-path mods-dmods/com.example.service.impl\service-impl/src/main/java/module-info.java\service-impl/src/main/java/com/example/service/impl/SimpleGreetingService.java编译 appmkdir-pmods/com.example.app javac --module-path mods-dmods/com.example.app\app/src/main/java/module-info.java\app/src/main/java/com/example/app/Main.java运行用 module-path -mjava--module-path mods\-mcom.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 本文原创转载请注明出处。