Java的Module System(JPMS):解决模块间的依赖与访问权限控制

好的,我们开始。

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 providesuses 关键字

providesuses 关键字用于实现 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 编译和运行

  1. 编译:

    javac -d mods/com.example.mymodule src/com.example.mymodule/module-info.java src/com.example.mymodule/com/example/mymodule/Main.java
  2. 创建模块路径:

    jlink --module-path mods --add-modules com.example.mymodule --output myapp
  3. 运行:

    ./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 项目迁移到模块化需要一些规划和工作。以下是一些步骤:

  1. 分析依赖关系: 确定项目中各个组件之间的依赖关系。可以使用工具来帮助分析。

  2. 创建 module-info.java 文件: 为每个模块创建 module-info.java 文件,声明模块的依赖关系、导出和服务。

  3. 解决循环依赖: 循环依赖是指两个或多个模块相互依赖。JPMS 不允许循环依赖,需要重新设计模块结构以消除循环依赖。

  4. 测试: 确保模块化后的应用程序仍然可以正常工作。

8. JPMS 的优势

使用 JPMS 可以带来以下好处:

  • 更强的封装性: 模块边界可以防止意外的依赖,提高代码的可靠性和可维护性。

  • 更好的依赖管理: 模块系统可以自动解析依赖关系,避免类路径问题。

  • 更小的运行时环境: 可以创建定制的运行时环境,只包含应用程序需要的模块,减少资源消耗和启动时间。

  • 更高的安全性: 模块边界可以限制恶意代码的访问权限,提高应用程序的安全性。

  • 改进的性能: 更小的运行时环境和更少的类加载可以提高应用程序的性能。

9. 总结:利用 JPMS 构建更优秀的 Java 应用

Java 9 引入的模块系统是一个重要的进步,它解决了传统 Java 类库管理和依赖关系处理中存在的一些问题。通过使用模块,我们可以构建更健壮、可维护、安全和高效的 Java 应用程序。虽然迁移到模块化需要一些工作,但带来的好处是值得的。理解和掌握 JPMS 对于现代 Java 开发至关重要。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注