Java模块系统全面解析
发布时间: 2025-08-18 02:22:41 阅读量: 2 订阅数: 7 

### Java模块系统全面解析
#### 1. 模块访问控制
在Java中,`exports` 和 `opens` 语句用于控制模块对包的访问。
##### 1.1 exports语句
`exports` 语句在编译时和运行时将模块的指定包导出到所有模块或指定的模块列表。它有以下两种形式:
- `exports <package>;`
- `exports <package> to <module1>, <module2>...;`
以下是使用 `exports` 语句的示例:
```java
module M {
exports com.jdojo.util;
exports com.jdojo.policy
to com.jdojo.claim, com.jdojo.billing;
}
```
##### 1.2 opens语句
`opens` 语句在运行时允许指定模块对指定包进行反射访问。其他模块可以使用反射访问指定包中的所有类型及其所有成员(包括私有和公共成员)。`opens` 语句有以下形式:
- `opens <package>;`
- `opens <package> to <module1>, <module2>...;`
以下是使用 `opens` 语句的示例:
```java
module M {
opens com.jdojo.claim.model;
opens com.jdojo.policy.model to core.hibernate;
opens com.jdojo.services to core.spring;
}
```
##### 1.3 exports和opens语句的比较
`exports` 语句仅允许在编译时和运行时访问指定包的公共API,而 `opens` 语句允许在运行时使用反射访问指定包中所有类型的公共和私有成员。
如果一个模块需要在编译时访问另一个模块的公共类型,并在运行时使用反射访问相同类型的私有成员,第二个模块可以同时导出和打开同一个包,示例如下:
```java
module N {
exports com.jdojo.claim.model;
opens com.jdojo.claim.model;
}
```
在阅读有关模块的内容时,会遇到以下三个短语:
- 模块M导出包P
- 模块M打开包Q
- 模块M包含包R
前两个短语对应模块中的 `exports` 和 `opens` 语句。第三个短语表示模块包含一个既未导出也未打开的名为R的包。在模块系统的早期设计中,第三种情况表述为“模块M隐藏包R”。
#### 2. 声明依赖
`requires` 语句声明当前模块对另一个模块的依赖。在名为M的模块中,`requires N` 语句表示模块M依赖(或读取)模块N。该语句有以下形式:
- `requires <module>;`
- `requires transitive <module>;`
- `requires static <module>;`
- `requires transitive static <module>;`
`requires` 语句中的 `static` 修饰符使依赖在编译时是必需的,但在运行时是可选的。在名为M的模块中,`requires static N` 语句表示模块M依赖于模块N,并且为了编译模块M,模块N必须在编译时存在;然而,为了使用模块M,模块N在运行时的存在是可选的。
`requires` 语句中的 `transitive` 修饰符使任何依赖于当前模块的模块隐式依赖于 `requires` 语句中指定的模块。假设有三个模块P、Q和R。假设模块Q包含 `requires transitive R` 语句。如果模块P包含 `requires Q` 语句,则表示模块P隐式依赖于模块R。
#### 3. 配置服务
Java允许使用服务提供者机制,其中服务提供者和服务消费者是解耦的。JDK 9允许使用 `uses` 和 `provides` 模块语句来实现服务。
##### 3.1 uses语句
`uses` 语句指定当前模块可以使用 `java.util.ServiceLoader` 类发现和加载的服务接口的名称。它的形式如下:
```java
uses <service-interface>;
```
以下是使用 `uses` 语句的示例:
```java
module M {
uses com.jdojo.prime.PrimeChecker;
}
```
这里,`com.jdojo.PrimeChecker` 是一个服务接口,其实现类将由其他模块提供。模块M将使用 `java.util.ServiceLoader` 类发现和加载该接口的实现。
##### 3.2 provides语句
`provides` 语句为服务接口指定一个或多个服务提供者实现类。它的形式如下:
```java
provides <service-interface>
with <service-impl-class1>, <service-impl-class2>...;
```
以下是使用 `provides` 语句的示例:
```java
module N {
provides com.jdojo.prime.PrimeChecker
with com.jdojo.prime.generic.GenericPrimeChecker;
}
```
同一个模块可以提供服务实现并发现和加载服务。一个模块还可以发现和加载一种类型的服务,并为另一种类型的服务提供实现,示例如下:
```java
module P {
uses com.jdojo.CsvParser;
provides com.jdojo.CsvParser
with com.jdojo.CsvParserImpl;
provides com.jdojo.prime.PrimeChecker
with com.jdojo.prime.generic.FasterPrimeChecker;
}
```
#### 4. 模块描述符
在了解如何声明模块后,可能会对模块声明的源代码有以下问题:
- 模块声明的源代码保存在哪里?是否保存在文件中?如果是,文件名是什么?
- 模块声明的源代码文件放在哪里?
- 模块声明的源代码如何编译?
##### 4.1 编译模块声明
模块声明存储在名为 `module-info.java` 的文件中,该文件存储在该模块的源文件层次结构的根目录下。Java编译器将模块声明编译为名为 `module-info.class` 的文件。`module-info.class` 文件称为模块描述符,它位于模块的编译代码层次结构的根目录下。如果将模块的编译代码打包到JAR文件中,`module-info.class` 文件将存储在JAR文件的根目录下。
模块声明不包含可执行代码。本质上,它包含模块的配置信息。为什么不将模块声明保存在XML或JSON格式的文本文件中,而是保存在类文件中呢?选择类文件作为模块描述符是因为类文件具有可扩展、定义良好的格式。模块描述符包含源级模块声明的编译形式。在模块声明最初编译后,工具(如 `jar` 工具)可以对其进行扩展,以在类文件属性中包含额外的信息。类文件格式还允许开发人员在模块声明中使用导入和注解。
##### 4.2 模块版本
在模块系统的初始原型中,模块声明还包括模块版本。在声明中包含模块版本使模块系统的实现变得复杂,因此模块版本从声明中移除。
利用模块描述符的可扩展格式(类文件格式)为模块添加版本。当将模块的编译代码打包到JAR中时,`jar` 工具提供了添加模块版本的选项,该版本最终会添加到 `module-info.class` 文件中。
#### 5. 模块源文件结构
以名为 `com.jdojo.contact` 的模块为例,该模块包含用于处理联系人信息(如地址和电话号码)的包,它包含两个包:
- `com.jdojo.contact.info`
- `com.jdojo.contact.validator`
`com.jdojo.contact.info` 包包含两个类:`Address` 和 `Phone`。`com.jdojo.contact.validator` 包包含一个名为 `Validator` 的接口和两个类 `AddressValidator` 和 `PhoneValidator`。
在Java 9中,Java编译器工具 `javac` 增加了几个选项,允许一次编译一个模块或多个模块。如果要一次编译多个模块,必须将每个模块的源代码存储在一个与模块名称相同的目录下。即使只有一个模块,也可以遵循这个源目录命名约定。
假设要编译 `com.jdojo.contact` 模块的源代码,可以将其源代码存储在名为 `C:\j9r\src` 的目录中,该目录包含以下文件:
```plaintext
module-info.java
com\jdojo\contact\info\Address.java
com\jdojo\contact\info\Phone.java
com\jdojo\contact\validator\Validator.java
com\jdojo\contact\validator\AddressValidator.java
com\jdojo\contact\validator\PhoneValidator.java
```
如果要一次编译多个模块,必须将源代码目录命名为 `com.jdojo.contact`,与模块名称相同。在这种情况下,可以将模块的源代码存储在名为 `C:\j9r\src` 的目录中,其内容如下:
```plaintext
com.jdojo.contact\module-info.java
com.jdojo.contact\com\jdojo\contact\info\Address.java
com.jdojo.contact\com\jdojo\contact\info\Phone.java
com.jdojo.contact\com\jdojo\contact\validator\Validator.java
com.jdojo.contact\com\jdojo\contact\validator\AddressValidator.java
com.jdojo.contact\com\jdojo\contact\validator\PhoneValidator.java
```
模块的编译代码将遵循与源代码相同的目录层次结构。
#### 6. 打包模块
模块的工件可以存储在以下位置:
- 目录
- 模块化JAR文件
- JMOD文件,这是JDK 9引入的一种新的模块打包格式
##### 6.1 存储在目录中的模块
当模块的编译代码存储在目录中时,目录的根目录包含模块描述符(`module-info.class` 文件),子目录反映包的层次结构。继续前面的示例,假设将 `com.jdojo.contact` 模块的编译代码存储在 `C:\j9r\mods\com.jdojo.contact` 目录中,该目录的内容如下:
```plaintext
module-info.class
com\jdojo\contact\info\Address.class
com\jdojo\contact\info\Phone.class
com\jdojo\contact\validator\Validator.class
com\jdojo\contact\validator\AddressValidator.class
com\jdojo\contact\validator\PhoneValidator.class
```
##### 6.2 存储在模块化JAR中的模块
JDK附带了一个 `jar` 工具,用于将Java代码打包成JAR(Java归档)文件格式。JAR格式基于ZIP文件格式。JDK 9增强了 `jar` 工具,使其能够将模块的代码打包到JAR中。当JAR包含模块的编译代码时,该JAR称为模块化JAR。模块化JAR的根目录包含一个 `module-info.class` 文件。
在JDK 9之前使用JAR的任何地方,现在都可以使用模块化JAR。例如,可以将模块化JAR放在类路径上,在这种情况下,模块化JAR中的 `module-info.class` 文件将被忽略,因为 `module-info` 不是Java中的有效类名。
在打包模块化JAR时,可以使用JDK 9中添加的 `jar` 工具的各种选项,向模块描述符中添加信息,如模块版本和主类名。
一个模块化JAR在各方面都是一个JAR,只是它的根目录包含一个模块描述符。通常,一个非平凡的Java应用程序由多个模块组成。一个模块化JAR只能包含一个模块的编译代码。需要将应用程序的所有模块打包到一个JAR中,以简化将应用程序作为一个工件进行分发。在撰写本文时,这是一个未解决的问题,相关描述可参考 [http://openjdk.java.net/projects/jigsaw/spec/issues/#MultiModuleExecutableJARs](http://openjdk.java.net/projects/jigsaw/spec/issues/#MultiModuleExecutableJARs)。
继续前面的示例,`com.jdojo.contact` 模块的模块化JAR的内容如下。请注意,JAR文件的 `META-INF` 目录中始终包含一个 `MANIFEST.MF` 文件。
```plaintext
module-info.class
com/jdojo/contact/info/Address.class
com/jdojo/contact/info/Phone.class
com/jdojo/contact/validator/Validator.class
com/jdojo/contact/validator/AddressValidator.class
com/jdojo/contact/validator/PhoneValidator.class
META-INF/MANIFEST.MF
```
##### 6.3 存储在JMOD文件中的模块
JDK 9引入了一种名为JMOD的新格式来打包模块。JMOD文件使用 `.jmod` 扩展名。JDK模块被编译为JMOD格式,并放在 `JDK_HOME\jmods` 目录中;例如,可以找到一个 `java.base.jmod` 文件,其中包含 `java.base` 模块的内容。JMOD文件仅在编译时和链接时受支持,在运行时不受支持。
#### 7. 模块路径
自JDK诞生以来,类路径机制就用于查找类型。类路径是一系列目录、JAR文件和ZIP文件。当Java在不同阶段(编译时、运行时、使用工具时等)需要查找类型时,它会使用类路径中的条目来查找该类型。
Java 9中的类型作为模块的一部分存在。Java在不同阶段需要查找模块,而不是像Java 9之前那样查找类型。Java 9引入了一种新的查找模块的机制,称为模块路径。
模块路径是一系列包含模块的路径名,路径名可以是模块化JAR、JMOD文件或目录的路径。路径名由特定于平台的路径分隔符分隔,在类UNIX平台上是冒号(:),在Windows平台上是分号(;)。
当路径名是模块化JAR或JMOD文件时,很容易理解。在这种情况下,如果JAR或JMOD文件中的模块描述符包含要查找的模块的定义,则找到该模块。如果路径名是目录,则存在以下两种情况:
- 如果目录的根目录存在类文件,则该目录被认为包含一个模块定义。根目录的类文件将被解释为模块描述符。所有其他文件和子目录将被解释为该模块的一部分。如果根目录存在多个类文件,则第一个找到的文件将被解释为模块描述符。经过一些实验,JDK 9 build 126似乎按字母排序选择第一个类文件。以这种方式存储模块的编译代码肯定会让人头疼。因此,如果目录的根目录包含多个类文件,应避免将该目录添加到模块路径中。
- 如果目录的根目录不存在类文件,则目录的内容将以不同的方式解释。目录中的每个模块化JAR或JMOD文件都被视为一个模块定义。每个子目录(如果其根目录包含一个 `module-info.class` 文件)被认为以展开的目录树格式包含一个模块定义。如果子目录的根目录不包含 `module-info.class` 文件,则不认为它包含模块定义。请注意,如果子目录包含模块定义,其名称不必与模块名称相同。模块名称从 `module-info.class` 文件中读取。
以下是Windows上有效的模块路径示例:
- `C:\mods`
- `C:\mods\com.jdojo.contact.jar;C:\mods\com.jdojo.person.jar`
- `C:\lib;C:\mods\com.jdojo.contact.jar;C:\mods\com.jdojo.person.jar`
第一个模块路径包含名为 `C:\mods` 的目录的路径。第二个模块路径包含两个模块化JAR的路径:`com.jdojo.contact.jar` 和 `com.jdojo.person.jar`。第三个模块路径包含三个元素:目录 `C:\lib` 的路径以及两个模块化JAR的路径:`com.jdojo.contact.jar` 和 `com.jdojo.person.jar`。在类UNIX平台上,这些路径的等效形式如下:
- `/usr/ksharan/mods`
- `/usr/ksharan/mods/com.jdojo.contact.jar:/usr/ksharan/com.jdojo.person.jar`
- `/usr/ksharan/lib:/usr/ksharan/mods/com.jdojo.contact.jar:/usr/ksharan/mods/com.jdojo.person.jar`
避免模块路径问题的最佳方法是不使用展开的目录作为模块定义。可以使用两个目录作为模块路径:一个目录包含所有应用程序的模块化JAR,另一个目录包含所有外部库的模块化JAR。例如,在Windows上可以使用 `C:\applib;C:\extlib` 作为模块路径,其中 `C:\applib` 目录包含所有应用程序的模块化JAR,`C:\extlib` 目录包含所有外部库的模块化JAR。
JDK 9更新了其所有工具,以使用模块路径来查找模块。这些工具提供了新的选项来指定模块路径。在JDK 9之前,常见的是使用以连字符(-)开头的UNIX风格的选项,例如 `-cp` 和 `-classpath`。由于JDK 9中增加了许多选项,JDK设计者在为选项寻找简短且对开发者有意义的名称时遇到了困难。因此,JDK 9开始使用GNU风格的选项,选项以两个连续的连字符开头,单词之间用连字符分隔。以下是一些GNU风格的命令行选项示例:
- `--class-path`
- `--module-path`
- `--module-version`
- `--main-class`
- `--print-module-descriptor`
要打印工具支持的所有标准选项列表,请使用 `--help` 或 `-h` 选项运行该工具;要查看所有非标准选项,请使用 `-X` 选项运行该工具。例如,`java -h` 和 `java -X` 命令将分别打印 `java` 命令的标准和非标准选项列表。
JDK 9中的大多数工具(如 `javac`、`java` 和 `jar`)支持在命令行上指定模块路径的两个选项:`-p` 和 `--module-path`。为了向后兼容,现有的UNIX风格选项将继续受支持。以下两个命令展示了如何使用这两个选项为 `java` 工具指定模块路径:
```plaintext
// 使用GNU风格的选项
C:\>java --module-path C:\applib;C:\lib other-args-go-here
// 使用UNIX风格的选项
C:\>java -p C:\applib;C:\extlib other-args-go-here
```
在所有示例中,使用GNU风格的选项 `--module-path` 来指定模块路径。使用GNU风格的选项时,可以通过以下两种形式之一指定选项的值:
- `--<name> <value>`
- `--<name>=<value>`
前面的命令也可以写成如下形式:
```plaintext
// 使用GNU风格的选项
C:\>java --module-path=C:\applib;C:\lib other-args-go-here
```
当使用空格作为名称 - 值分隔符时,至少需要使用一个空格。当使用 `=` 作为分隔符时,不能在其周围包含任何空格。选项 `--module-path=C:\applib` 是有效的。选项 `--module-path =C:\applib` 是无效的,因为 `=C:\applib` 将被解释为模块路径,这是一个无效的路径。
综上所述,Java 9的模块系统为Java开发带来了许多新特性和改进,包括模块访问控制、依赖声明、服务配置、模块描述符、模块源文件结构、打包模块和模块路径等方面。通过合理使用这些特性,可以更好地组织和管理Java项目,提高代码的可维护性和可扩展性。
### Java模块系统全面解析
#### 8. 模块系统关键特性总结
为了更清晰地理解Java 9模块系统的各个部分,下面通过表格形式对前面介绍的关键特性进行总结:
| 特性 | 描述 | 示例 |
| --- | --- | --- |
| `exports` 语句 | 在编译时和运行时将模块的指定包导出到所有模块或指定的模块列表 | ```java module M { exports com.jdojo.util; exports com.jdojo.policy to com.jdojo.claim, com.jdojo.billing; } ``` |
| `opens` 语句 | 在运行时允许指定模块对指定包进行反射访问 | ```java module M { opens com.jdojo.claim.model; opens com.jdojo.policy.model to core.hibernate; opens com.jdojo.services to core.spring; } ``` |
| `requires` 语句 | 声明当前模块对另一个模块的依赖 | ```java module M { requires N; requires transitive R; requires static N; requires transitive static R; } ``` |
| `uses` 语句 | 指定当前模块可以使用 `java.util.ServiceLoader` 类发现和加载的服务接口的名称 | ```java module M { uses com.jdojo.prime.PrimeChecker; } ``` |
| `provides` 语句 | 为服务接口指定一个或多个服务提供者实现类 | ```java module N { provides com.jdojo.prime.PrimeChecker with com.jdojo.prime.generic.GenericPrimeChecker; } ``` |
| 模块描述符 | 存储在 `module-info.class` 文件中,包含模块的配置信息 | 存储在模块编译代码层次结构的根目录,JAR 包中存储在根目录 |
| 模块版本 | 利用模块描述符的可扩展格式添加,打包 JAR 时可通过 `jar` 工具添加 | |
| 模块源文件结构 | 源文件存储遵循包层次结构,`module-info.java` 存储在源文件层次结构根目录 | ```plaintext com.jdojo.contact\module-info.java com.jdojo.contact\com\jdojo\contact\info\Address.java ... ``` |
| 打包模块 | 可存储在目录、模块化 JAR 文件、JMOD 文件中 | 目录:```plaintext module-info.class com\jdojo\contact\info\Address.class ... ``` 模块化 JAR:```plaintext module-info.class com/jdojo/contact/info/Address.class ... META-INF/MANIFEST.MF ``` JMOD:使用 `.jmod` 扩展名 |
| 模块路径 | 一系列包含模块的路径名,用于查找模块 | Windows:`C:\mods;C:\mods\com.jdojo.contact.jar` 类 UNIX:`/usr/ksharan/mods:/usr/ksharan/mods/com.jdojo.contact.jar` |
#### 9. 模块系统使用流程
下面通过 mermaid 格式的流程图展示使用 Java 9 模块系统开发项目的一般流程:
```mermaid
graph TD;
A[定义模块结构] --> B[编写模块代码];
B --> C[编写 module-info.java];
C --> D[编译模块代码];
D --> E[打包模块];
E --> F[配置模块路径];
F --> G[运行项目];
```
具体步骤如下:
1. **定义模块结构**:确定项目包含哪些模块,每个模块包含哪些包和类。例如前面提到的 `com.jdojo.contact` 模块,包含 `com.jdojo.contact.info` 和 `com.jdojo.contact.validator` 包。
2. **编写模块代码**:根据模块结构编写各个类和接口的代码。
3. **编写 `module-info.java`**:在模块的源文件层次结构根目录创建 `module-info.java` 文件,使用 `exports`、`opens`、`requires`、`uses`、`provides` 等语句进行模块配置。
4. **编译模块代码**:使用 `javac` 工具编译模块代码,注意可使用新的选项指定模块路径。例如:
```plaintext
javac --module-path C:\applib;C:\extlib -d C:\j9r\mods com.jdojo.contact\module-info.java com.jdojo.contact\com\jdojo\contact\info\Address.java ...
```
5. **打包模块**:可以选择将模块打包成目录、模块化 JAR 文件或 JMOD 文件。例如使用 `jar` 工具打包成模块化 JAR:
```plaintext
jar --create --file C:\j9r\mods\com.jdojo.contact.jar --module-version 1.0 -C C:\j9r\mods com.jdojo.contact
```
6. **配置模块路径**:在运行项目时,使用 `-p` 或 `--module-path` 选项指定模块路径。例如:
```plaintext
java --module-path C:\applib;C:\extlib --module com.jdojo.contact/com.jdojo.contact.Main
```
7. **运行项目**:使用 `java` 命令运行项目。
#### 10. 常见问题及解决方法
在使用 Java 9 模块系统时,可能会遇到一些常见问题,下面列举并给出解决方法:
- **问题 1:找不到模块**
- **原因**:模块路径配置错误,或者模块描述符中模块定义有问题。
- **解决方法**:检查模块路径是否正确,确保路径中包含所需的模块化 JAR、JMOD 文件或目录。检查 `module-info.java` 文件中模块名称和依赖是否正确。
- **问题 2:反射访问失败**
- **原因**:没有使用 `opens` 语句打开相应的包。
- **解决方法**:在 `module-info.java` 中使用 `opens` 语句打开需要反射访问的包。
- **问题 3:打包多个模块到一个 JAR 失败**
- **原因**:目前这是一个未解决的问题,JDK 尚未完全支持。
- **解决方法**:关注 [http://openjdk.java.net/projects/jigsaw/spec/issues/#MultiModuleExecutableJARs](http://openjdk.java.net/projects/jigsaw/spec/issues/#MultiModuleExecutableJARs) 上的进展,等待官方解决方案。
#### 11. 总结与展望
Java 9 的模块系统为 Java 开发带来了重大的改进,通过模块化的设计,使得 Java 项目的组织和管理更加清晰和高效。模块访问控制机制增强了代码的安全性,依赖声明和服务配置使得模块之间的关系更加明确,模块描述符和模块路径的引入为模块的编译、打包和运行提供了统一的标准。
然而,模块系统仍然存在一些待解决的问题,如多模块可执行 JAR 的打包问题。未来,随着 Java 的不断发展,相信这些问题会得到解决,模块系统也会更加完善。开发者可以充分利用 Java 9 模块系统的特性,提高代码的质量和可维护性,为构建更复杂、更健壮的 Java 应用程序奠定基础。
通过合理运用模块系统的各个特性,结合实际项目需求,开发者可以更好地应对大规模项目的开发挑战,实现代码的高内聚、低耦合,提高开发效率和代码的可扩展性。同时,不断关注 Java 社区的动态,学习最新的开发技巧和最佳实践,将有助于在 Java 开发领域保持领先地位。
0
0
相关推荐









