好的,我们开始。
Java 9 模块系统 (JPMS):构建更健壮、可维护的应用
大家好,今天我们来深入探讨 Java 9 引入的模块系统,也称为 JPMS(Java Platform Module System)或 Project Jigsaw。在 Java 9 之前,Java 类库的管理和依赖关系处理一直存在一些问题。JPMS 旨在解决这些问题,提高代码的可靠性、安全性,并优化运行时性能。
1. 模块化之前的困境
在 Java 8 及更早版本中,我们主要面临以下几个问题:
-
缺乏强封装性: 所有的 public 类和方法都对所有其他类可见,即使它们不应该被访问。这导致了类之间的意外依赖,使得重构变得困难。
-
类路径 (Classpath) 问题: 类路径是一个扁平的结构,所有的类都位于同一个命名空间。这容易导致类名冲突,特别是在大型项目中,依赖管理变得复杂。
-
臃肿的运行时环境: 应用程序通常需要加载整个 Java 运行时环境 (JRE),即使只使用了其中的一部分。这导致了资源浪费和启动时间延长。
2. JPMS 的核心概念
JPMS 通过引入模块的概念来解决这些问题。一个模块是一个自包含的单元,它定义了以下内容:
-
模块名称: 模块的唯一标识符。
-
导出 (Exports): 模块公开给其他模块的包。只有导出的包中的 public 类型才能被其他模块访问。
-
依赖 (Requires): 模块依赖的其他模块。
-
服务提供 (Provides): 模块提供的服务接口的实现。
-
服务消费 (Uses): 模块使用的服务接口。
模块信息存储在一个名为 module-info.java 的描述符文件中,该文件位于模块的根目录下。
3. module-info.java 详解
module-info.java 文件是模块定义的核心。让我们通过一个示例来了解它的结构和语法:
module com.example.mymodule {
requires java.sql; // 依赖 java.sql 模块
exports com.example.mymodule.api; // 导出 com.example.mymodule.api 包
opens com.example.mymodule.internal to another.module; // 打开包给另一个模块进行反射访问
provides com.example.service.MyService with com.example.mymodule.impl.MyServiceImpl; //提供服务实现
uses com.example.service.MyService; // 使用服务接口
}
让我们逐行解释:
-
module com.example.mymodule: 定义模块的名称。模块名称应该遵循反向域名命名约定,以避免冲突。 -
requires java.sql: 声明对java.sql模块的依赖。这意味着com.example.mymodule中的代码可以使用java.sql模块中导出的类型。 -
exports com.example.mymodule.api: 将com.example.mymodule.api包中的 public 类型导出给其他模块。只有导出的包中的 public 类型才能被其他模块访问。 -
opens com.example.mymodule.internal to another.module: 允许another.module模块对com.example.mymodule.internal包进行反射访问。默认情况下,即使是 public 类型,如果未导出或打开,也不能被反射访问。opens语句允许特定的模块进行反射访问。 如果不指定to module,则对所有模块开放反射访问。 -
provides com.example.service.MyService with com.example.mymodule.impl.MyServiceImpl: 声明该模块提供com.example.service.MyService接口的com.example.mymodule.impl.MyServiceImpl实现。 这是 Java 的服务加载机制的一部分,允许模块动态发现和使用服务实现。 -
uses com.example.service.MyService: 声明该模块使用com.example.service.MyService接口。
3.1 requires 关键字
requires 关键字用于声明模块的依赖关系。Java 模块系统会自动解析这些依赖关系,并确保所有必需的模块都可用。
| 关键字 | 描述 |
|---|---|
requires |
声明对另一个模块的依赖。 |
requires transitive |
声明对另一个模块的依赖,并且将该依赖传递给依赖当前模块的模块。例如,模块 A requires transitive 模块 B,模块 C requires 模块 A,那么模块 C 也可以访问模块 B 导出的类型。 |
requires static |
声明一个静态依赖。这意味着该依赖只在编译时需要,运行时可能不需要。这通常用于处理可选的依赖关系。 如果运行时不存在,则不报错。 |
module com.example.modulea {
requires java.sql;
requires transitive com.example.moduleb;
}
module com.example.moduleb {
exports com.example.moduleb.api;
}
module com.example.modulec {
requires com.example.modulea; // 隐式依赖 com.example.moduleb
}
3.2 exports 关键字
exports 关键字用于声明模块导出的包。只有导出的包中的 public 类型才能被其他模块访问。这提供了强封装性,防止了不必要的依赖。
| 关键字 | 描述 |
|---|---|
exports |
将指定的包导出给所有其他模块。 |
exports to |
将指定的包导出给特定的模块。只有指定的模块才能访问导出的包中的 public 类型。 |
module com.example.modulea {
exports com.example.modulea.api;
exports com.example.modulea.internal to com.example.moduleb;
}
3.3 opens 关键字
opens 关键字用于允许其他模块对指定包中的类型进行反射访问。默认情况下,即使是 public 类型,如果未导出,也不能被反射访问。
| 关键字 | 描述 |
|---|---|
opens |
允许所有模块对指定的包进行反射访问。 |
opens to |
允许特定的模块对指定的包进行反射访问。 |
module com.example.modulea {
opens com.example.modulea.config;
opens com.example.modulea.internal to com.example.moduleb;
}
3.4 provides 和 uses 关键字
provides 和 uses 关键字用于实现 Java 的服务加载机制。
provides关键字声明一个模块提供某个接口的实现。uses关键字声明一个模块使用某个接口。
module com.example.modulea {
provides com.example.service.MyService with com.example.modulea.impl.MyServiceImpl;
}
module com.example.moduleb {
uses com.example.service.MyService;
public void doSomething() {
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
for (MyService service : loader) {
service.execute();
}
}
}
4. 创建和编译模块化应用
现在,让我们创建一个简单的模块化应用程序,以演示 JPMS 的使用。
4.1 项目结构
my-module-app/
├── module-info.java
└── com
└── example
└── mymodule
└── Main.java
4.2 module-info.java
module com.example.mymodule {
requires java.base; // 默认依赖,可以省略
exports com.example.mymodule;
}
4.3 Main.java
package com.example.mymodule;
public class Main {
public static void main(String[] args) {
System.out.println("Hello, Modular World!");
}
}
4.4 编译和运行
-
编译:
javac -d mods/com.example.mymodule src/com.example.mymodule/module-info.java src/com.example.mymodule/com/example/mymodule/Main.java -
创建模块路径:
jlink --module-path mods --add-modules com.example.mymodule --output myapp -
运行:
./myapp/bin/java --module com.example.mymodule/com.example.mymodule.Main
这将输出 "Hello, Modular World!"。
5. 模块路径 (Module Path) 和类路径 (Classpath)
JPMS 引入了模块路径的概念,它与类路径类似,但用于查找模块而不是类。模块路径是一个包含模块化 JAR 文件或展开模块目录的路径列表。
-
模块路径 (-p 或 –module-path): 用于查找模块。JVM 将查找模块描述符
module-info.java文件来确定模块的依赖关系和导出。 -
类路径 (-cp 或 –classpath): 用于查找传统的 JAR 文件和类文件。
重要区别:
- 在模块路径上,JVM 会强制执行模块边界,确保模块只能访问其依赖模块中导出的类型。
- 在类路径上,所有的类都位于同一个命名空间,没有模块边界的限制。
6. 自动模块 (Automatic Modules)
为了方便现有项目迁移到模块化,JPMS 引入了自动模块的概念。自动模块是指那些没有 module-info.java 文件的 JAR 文件。当 JAR 文件被放置在模块路径上时,JVM 会自动将其转换为一个模块。
自动模块的名称由 JAR 文件的名称派生而来。例如,mylibrary.jar 将被转换为名为 mylibrary 的自动模块。
自动模块的特点:
- 自动模块会导出其所有包。
- 自动模块会读取模块路径上的所有其他模块。
自动模块提供了一种过渡方案,但建议最终将项目转换为显式模块,以获得 JPMS 的全部好处。
7. 迁移到模块化
将现有的 Java 项目迁移到模块化需要一些规划和工作。以下是一些步骤:
-
分析依赖关系: 确定项目中各个组件之间的依赖关系。可以使用工具来帮助分析。
-
创建
module-info.java文件: 为每个模块创建module-info.java文件,声明模块的依赖关系、导出和服务。 -
解决循环依赖: 循环依赖是指两个或多个模块相互依赖。JPMS 不允许循环依赖,需要重新设计模块结构以消除循环依赖。
-
测试: 确保模块化后的应用程序仍然可以正常工作。
8. JPMS 的优势
使用 JPMS 可以带来以下好处:
-
更强的封装性: 模块边界可以防止意外的依赖,提高代码的可靠性和可维护性。
-
更好的依赖管理: 模块系统可以自动解析依赖关系,避免类路径问题。
-
更小的运行时环境: 可以创建定制的运行时环境,只包含应用程序需要的模块,减少资源消耗和启动时间。
-
更高的安全性: 模块边界可以限制恶意代码的访问权限,提高应用程序的安全性。
-
改进的性能: 更小的运行时环境和更少的类加载可以提高应用程序的性能。
9. 总结:利用 JPMS 构建更优秀的 Java 应用
Java 9 引入的模块系统是一个重要的进步,它解决了传统 Java 类库管理和依赖关系处理中存在的一些问题。通过使用模块,我们可以构建更健壮、可维护、安全和高效的 Java 应用程序。虽然迁移到模块化需要一些工作,但带来的好处是值得的。理解和掌握 JPMS 对于现代 Java 开发至关重要。