java模块化架构中应按业务域而非技术层划分模块,每个模块是语义自洽、边界清晰的“业务小王国”,通过module-info.java精确声明依赖与导出,接口定义在提供方、实现可插拔,di负责运行时组装,jpms保障编译期约束。

在 Java 模块化架构(JPMS)中设计高内聚低耦合的单元,关键不是把类按技术层塞进模块,而是让每个模块成为一个语义自洽、边界清晰、可独立演进的“业务小王国”。它既不是包的放大版,也不是功能的简单归类,而是一组职责紧密关联、对外仅暴露精炼契约、对内完全掌控实现细节的有机整体。
按业务域切分,而非按技术层
一个模块应围绕一个明确的业务能力组织,比如 com.example.order 负责订单全生命周期,com.example.inventory 管理库存水位与扣减逻辑。模块内部可以自由采用 MVC、DDD 分层或函数式风格,但对外只导出 api 子包(如 exports com.example.order.api;)。绝不导出 impl、internal、config 或 model 包——这些属于实现细节,本就不该被外部感知。
- 订单模块不提供“发短信”或“写日志”的能力,那是通知模块和日志模块的事
- 避免出现
common-utils这类通用模块:日期格式化应由订单模块自己封装,或由 JDKjava.time直接承担 - 如果两个模块总是一起修改、一起发布,说明它们本该是一个模块;如果一个模块被十几个其他模块
requires,大概率它已变成“上帝模块”,需要拆解
用 module-info.java 强声明边界与契约
module-info.java 是模块的宪法,不是装饰性配置。它必须精准表达“我依赖谁”和“我向谁承诺什么”:
- 用
requires声明直接依赖,仅限真正调用其 API 的模块;若只是内部使用(如日志框架仅用于打印),不强制要求下游也依赖它 - 用
requires transitive表达“我提供的接口类型里用了它的类”,例如订单 API 方法返回PaymentResult,就必须requires transitive com.example.payment;,否则调用方编译失败 - 禁止循环依赖:A
requiresB 且 BrequiresA —— JPMS 编译器会立刻报错,这是天然的架构守门员 - 不导出任何含
if (channel.equals("sms"))或throws RedisException的接口包,那说明契约已泄漏实现
接口定义在提供方,实现可插拔
高内聚要求行为与数据靠近,低耦合要求调用方不绑定实现。因此:
- 接口定义和
exports必须在能力提供方模块(如com.example.notification导出NotificationService) - 具体实现可放在消费方模块(如订单模块自己写
EmailNotificationServiceImpl)、独立实现模块(com.example.notification.email),或由提供方提供默认实现 - 业务模块通过
ServiceLoader.load(NotificationService.class)获取实例,或交由 DI 容器(如 Spring)组装,完全不感知实现来源 - 新增一种通知渠道,只需新增实现模块 + 在其
module-info.java中声明provides NotificationService with EmailSender;,原订单模块零改动
运行时组装交给 DI,编译期约束交给 JPMS
JPMS 负责划清类加载边界和编译期可见性,DI 框架负责对象协作关系。两者分工明确:
- 模块声明“我能提供什么”,容器决定“谁来提供、何时提供、如何组合”
- 构造器注入 +
final字段是底线:业务类(如OrderService)只持有接口类型,不可变、可测、无隐藏依赖 - 单元测试时,直接传入
new MockNotificationService(),不启动 Spring,也不加载模块路径 - 避免在类内部
new XxxImpl()、Class.forName()或静态工具方法调用——这些都会绕过模块边界和 DI 约束
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











