1. 什么是 module-info.java,它在哪里?
Java 中的模块总是从文件 module-info.java 开始。这相当于模块的“身份证”,你在其中声明其名称、要导出的包以及对其他模块的依赖。
在哪里找到?
文件 module-info.java 应位于该模块源码目录的根下。例如,如果你的项目结构如下:
project-root/
└── src/
└── my.module.name/
├── module-info.java
└── com/
└── example/
└── api/
└── MyClass.java
这里 my.module.name 是模块名(命名规则稍后介绍)。
没有这个文件,模块就不算模块!
如果没有它——那就是一个传统的“老式” Java 项目,即便有一堆目录和类。
2. module-info.java 语法:核心元素
我们先看一个最简单的示例——真的不难,放心!
module my.module.name {
exports com.example.api;
requires java.sql;
}
逐段拆解:
module my.module.name { ... }
这是一个名为 my.module.name 的模块声明。模块名是唯一标识,通常与根包一致(例如 com.example.app)。
趣闻:如果你把模块命名为 java.base,编译器可不会高兴。不要尝试冒充标准模块。
exports com.example.api;
这行表示:模块会公开包 com.example.api 中的内容。该包内所有标记为 public 的内容对其他模块可见,其余仅对本模块可见。
requires java.sql;
这里我们向编译器“坦白”:“我的工作需要标准模块 java.sql。”否则,编译器不允许我们使用该模块中的类。
附加关键字(拓展了解)
- opens <package>; —— 为反射打开包(例如用于 Jackson 一类的序列化库)。
- uses <service-interface>; —— 声明模块会使用某个服务(接口)。
- provides <service-interface> with <implementation-class>; —— 声明模块提供该服务的实现。
在本讲中,我们将重点关注 exports 和 requires —— 它们在绝大多数教学和生产项目中都用得上。
3. module-info.java 示例
示例 1:最小模块
module com.example.hello {
exports com.example.hello.api;
}
- 此模块仅导出包 com.example.hello.api。
- 例如,位于 com.example.hello.internal 的内容将对其他模块隐藏,即使其中有 public 类。
示例 2:带依赖的模块
module com.example.dbclient {
exports com.example.db.api;
requires java.sql;
}
我们可以使用 JDBC,但前提是已如实声明对 java.sql 的依赖。
示例 3:导出多个包
module com.example.library {
exports com.example.library.api;
exports com.example.library.utils;
}
可以导出任意数量的包(但不要什么都导出——这违背了模块化的意义!)。
4. 限制与规则
模块名
- 通常与根包一致(例如 com.example.app)。
- 不能与标准模块名称相同(java.base、java.sql 等)。
- 不应包含空格、特殊字符,且不能以数字开头等。
- 建议:使用组织或项目的反向域名以避免冲突。
一模块一份 module-info.java
一个模块中只能有一个这样的文件。如果有两个——编译器会给你来一场“模块风波”。
同一包只能由一个模块导出
同一个包不能由两个不同的模块导出。这就像同一个人持有两本同名护照——官方不会认可。
模块内的包
只能导出在该模块源码结构中实际存在的包。导出不存在的包将导致编译错误。
5. 实践:在项目中创建 module-info.java
假设我们有一个简单项目,目录如下:
project-root/
└── src/
└── com.example.greetings/
├── module-info.java
└── com/
└── example/
└── greetings/
├── api/
│ └── Greeter.java
└── internal/
└── SecretSauce.java
步骤 1:创建 module-info.java
module com.example.greetings {
exports com.example.greetings.api;
}
步骤 2:尝试在另一个模块中使用 internal 包里的类
假设我们还有第二个模块 com.example.app,想访问 SecretSauce:
module com.example.app {
requires com.example.greetings;
}
import com.example.greetings.internal.SecretSauce; // 错误!
结果:
编译器会说:“包 com.example.greetings.internal 未被模块 com.example.greetings 导出”。即便类 SecretSauce 是 public,它对其他模块也是不可见的。
这就是模块层面的真正封装!
步骤 3:尝试不声明 requires
如果在 com.example.app 中没有写 requires com.example.greetings;,却尝试使用 com.example.greetings.api 中的类,编译器会报错:
package com.example.greetings.api is not visible
6. 使用 module-info.java 时的常见错误
错误 1:模块名与项目结构不匹配。
如果你把模块命名为 com.example.app,而目录结构却是 src/main/java/app,编译器就不知道你想要什么。模块名通常与根包一致,目录也应反映这一点。
错误 2:把一切都导出。
只应导出确实需要对其他模块可见的内容。不要因为“更省事”就写 exports com.example;。这会破坏封装性。
错误 3:忘记添加 requires。
如果你使用了其他模块或标准库(例如 java.sql)中的类,却忘了声明依赖——就会出现编译错误。
错误 4:在 exports 中导出不存在的包。
如果你写了 exports com.example.foo;,但该包并不存在——编译器会提示你在“导出空气”。
错误 5:未导出包中的 public 类。
如果类被声明为 public,但所在的包没有导出,那么该类只能在模块内部可见。这不算错误,但常常让新手意外。
错误 6:一个模块中有多个 module-info.java。
一个模块中应只有一个 module-info.java 文件。如果有两个——编译器将无法构建项目。
GO TO FULL VERSION