
本文详解如何在 jpms(java platform module system)下合理设计基于 serviceloader 的模块化架构,重点解决 api 模块导出策略、服务可扩展性与封装边界的平衡问题。
本文详解如何在 jpms(java platform module system)下合理设计基于 serviceloader 的模块化架构,重点解决 api 模块导出策略、服务可扩展性与封装边界的平衡问题。
在构建可扩展的模块化 Java 应用时,采用 Service Provider Interface(SPI) 模式配合 java.util.ServiceLoader 是业界公认的解耦核心与插件的标准方案。你的设计思路——将 app.view.View 和 app.engine.Engine 抽象为独立 API 模块,并由主模块 app 组合装配——完全符合模块化分层原则。但围绕 exports 与 exports to 的疑虑,恰恰触及了 JPMS 中“可见性(readability)”与“可实现性(servicability)”的核心权衡。
✅ 正确做法:无限制导出(exports)是必要且安全的
你无需担忧 exports app.view 会导致该包被“任意模块随意访问”。JPMS 的模块可见性是显式声明驱动的:一个模块只有在其 module-info.java 中明确声明 requires app.view,才能访问其导出的类型。更重要的是——依赖关系不具备传递性。例如:
// module-info.java of 'app'
module app {
requires app.view; // ✅ app 可见 View 接口
requires app.engine; // ✅ app 可见 Engine 接口
uses app.view.View; // ✅ 声明使用 View 服务
uses app.engine.Engine;// ✅ 声明使用 Engine 服务
}此时,即使存在另一个无关模块 com.example.utils,只要它未声明 requires app.view,就完全无法访问 View 类型——哪怕 app.view 使用了 exports 而非 exports to。这与 requires transitive 的行为有本质区别(后者才会将依赖“透传”给下游模块)。
⚠️ 为什么 exports to 不适用于开放 SPI 场景?
exports to app.view 语法会将包的可见性严格限定于指定模块列表,例如:
立即学习“Java免费学习笔记(深入)”;
// module-info.java of app.view —— ❌ 错误示例
module app.view {
exports app.view to app; // 仅 app 可见
}这看似增强了封装,却直接破坏了 SPI 的扩展前提:第三方实现模块必须能编译并声明 provides app.view.View with ...,而这要求它们能读取 app.view.View 类型。若 app.view 仅导出给 app,则任何外部实现模块因无法 requires app.view(编译失败),根本无法提供服务。此时 SPI 形同虚设。
? 关键结论:SPI 的 API 模块必须使用无限制 exports,这是实现“开放封闭”原则的技术前提——对扩展开放(任何人可实现),对修改封闭(API 稳定不变)。
✅ 推荐模块结构与配置示例
| 模块 | module-info.java 片段 | 说明 |
|---|---|---|
| app.view | module app.view { exports app.view; } | 纯 API 模块,定义 View 接口,不限制导出范围 |
| app.engine | module app.engine { exports app.engine; } | 纯 API 模块,定义 Engine 接口 |
| app | requires app.view; requires app.engine; uses app.view.View; uses app.engine.Engine; | 主程序模块,消费服务,不提供实现 |
| app.view.swing(第三方) | requires app.view; provides app.view.View with com.swing.SwingView; | 实现模块,需 requires API 模块才能编译 |
运行时,通过 ServiceLoader.load(View.class) 即可动态发现所有已安装的 View 实现,无需硬编码或配置文件。
? 注意事项与最佳实践
- 避免在 API 模块中暴露实现细节:app.view 模块内不应包含具体 UI 组件类,只保留接口、注解和 DTO;
- 版本兼容性优先:SPI 接口一旦发布,应遵循语义化版本规则,避免破坏性变更;
- 考虑服务加载时机:在 app 启动早期完成 ServiceLoader 初始化,确保服务实例可用;
- 日志与错误处理:捕获 ServiceConfigurationError 并记录详细信息,便于排查第三方实现模块的加载失败原因。
综上,你最初的设计思想是健壮且符合 JPMS 哲学的。exports 非但不是设计失误,反而是支撑开放生态的基石。真正的封装不在于隐藏 API,而在于清晰界定契约边界——让模块各司其职,用 requires/uses/provides 显式表达协作意图,这才是模块化架构的优雅所在。


















