Java 9 模块化:如何使用模块化提升代码管理

Java 9 引入了一个非常重要的特性——模块化系统,这是 Java 平台发展的一个重大进步。模块化不仅改变了 Java 项目的构建和部署方式,还能显著提升代码的可管理性、可维护性、可扩展性以及安全性。在这篇文章中,我们将深入探讨 Java 9 的模块化特性,并通过详细的代码示例展示如何使用模块化提升代码管理。

Java 模块化的基础概念

什么是 Java 模块?

在 Java 9 之前,Java 应用程序的构建和管理是基于包(Package)的,而模块化的引入,使得 Java 程序可以按功能进行组织。一个模块是一个包含包、类、接口和其他资源的自包含单位。每个模块都有一个明确的依赖声明,并且能够清晰地暴露哪些资源可以被其他模块访问。

模块的组成

Java 9 中的模块主要由两个文件组成:

  1. 模块描述符(module-info.java):模块的核心配置文件,包含模块名称、所依赖的其他模块以及暴露的包。
  2. 模块内部的类和资源:这些类和资源在模块内部进行管理,并且可以通过模块描述符控制哪些是公开的,哪些是私有的。

创建一个简单的 Java 模块

接下来,我们将通过一个简单的示例来展示如何创建一个模块,并使用模块化来管理 Java 代码。

第一步:创建一个模块

我们创建一个简单的模块,模块名为 com.example.utils,它包含一个计算器类。首先,在项目根目录下创建一个 src 文件夹,并为模块创建相应的目录结构。

src/
└── com.example.utils/
    ├── module-info.java
    └── com/
        └── example/
            └── utils/
                └── Calculator.java
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
1. 创建 module-info.java

模块描述符文件 module-info.java 位于模块的根目录下。我们在该文件中定义模块名称,并声明它的公共 API。

// module-info.java
module com.example.utils {
    exports com.example.utils;
}
  • 1.
  • 2.
  • 3.
  • 4.

在上面的代码中,module 关键字用于声明模块 com.example.utils,而 exports 关键字表示模块暴露了 com.example.utils 包供其他模块使用。

2. 创建一个类 Calculator.java

接下来,我们在 com.example.utils 包中创建一个简单的 Calculator 类,用于执行一些基本的数学操作。

// Calculator.java
package com.example.utils;

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public int subtract(int a, int b) {
        return a - b;
    }

    public int multiply(int a, int b) {
        return a * b;
    }

    public int divide(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException("Cannot divide by zero");
        }
        return a / b;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
第二步:创建使用模块的应用

现在,我们创建一个新的 Java 应用程序,它将使用 com.example.utils 模块中的 Calculator 类。

1. 创建应用程序模块

在应用程序的源代码目录中创建一个新的模块 com.example.app,并在该模块中引用 com.example.utils 模块。

src/
└── com.example.app/
    ├── module-info.java
    └── com/
        └── example/
            └── app/
                └── Main.java
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
2. 创建 module-info.java

com.example.app 模块中,我们声明依赖于 com.example.utils 模块。

// module-info.java
module com.example.app {
    requires com.example.utils;
}
  • 1.
  • 2.
  • 3.
  • 4.
3. 创建 Main.java

com.example.app 中创建一个 Main 类,使用 com.example.utils 模块提供的 Calculator 类。

// Main.java
package com.example.app;

import com.example.utils.Calculator;

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        int sum = calculator.add(5, 3);
        System.out.println("Sum: " + sum);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
第三步:编译和运行模块化 Java 应用
  1. 编译模块

使用以下命令编译 com.example.utilscom.example.app 模块:

javac -d out --module-source-path src $(find src -name "*.java")
  • 1.
  1. 运行模块

使用以下命令运行模块化的应用程序:

java --module-path out --module com.example.app/com.example.app.Main
  • 1.

这时,输出应该是:

Sum: 8
  • 1.

Java 模块化的优势

1. 增强的代码封装性

通过模块化,可以严格控制哪些类和包对外暴露。只有明确声明 exports 的包才能被其他模块访问,这有效防止了不必要的类暴露,增强了封装性。

2. 更强的模块依赖管理

模块化系统可以帮助我们清晰地管理模块之间的依赖关系。在 module-info.java 中,我们可以使用 requires 关键字声明模块之间的依赖,使得每个模块的依赖关系显而易见,并能够确保依赖项的版本正确。

3. 提升性能

通过模块化,可以构建更轻量的应用程序。Java 9 的模块化系统允许按需加载模块,这意味着应用程序只加载实际需要的模块,而不必加载所有模块,从而减少了内存消耗。

4. 强化安全性

模块化使得我们能够限制某些类或包的访问权限。通过模块化配置文件,我们可以明确规定哪些类对外部是不可见的,避免了外部访问未授权的实现细节。

模块间的依赖管理

在 Java 9 中,模块化的核心概念之一就是模块之间的依赖关系管理。在 module-info.java 文件中,除了 exportsrequires,我们还可以使用其他关键字来定义模块的行为,管理模块之间的关系。

使用 requires transitive

requires transitive 允许某个模块的依赖传播给其他依赖该模块的模块。换句话说,如果模块 A 依赖模块 B,而模块 B 使用了 requires transitive,那么模块 C(依赖模块 A)将自动也获得模块 B 的访问权限。

示例:使用 transitive 关键字

我们在 com.example.utils 模块中使用第三方库,比如 org.apache.commons.lang3,并希望所有依赖 com.example.utils 的模块自动拥有对该库的访问权限。我们可以使用 requires transitive 来实现这一点。

  1. 修改 com.example.utils 模块的 module-info.java 文件
// module-info.java in com.example.utils
module com.example.utils {
    requires transitive org.apache.commons.lang3;  // 添加 transitive 依赖
    exports com.example.utils;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  1. 创建一个新的模块,com.example.app

com.example.app 模块中,我们并没有直接声明对 org.apache.commons.lang3 的依赖,但由于 com.example.utils 使用了 transitive,它自动将 org.apache.commons.lang3 作为依赖传递过来。

// module-info.java in com.example.app
module com.example.app {
    requires com.example.utils; // 通过 com.example.utils 获取 Apache Commons Lang
}
  • 1.
  • 2.
  • 3.
  • 4.
  1. 使用 org.apache.commons.lang3

com.example.app 中,我们可以直接使用 Apache Commons Lang 库中的功能:

// Main.java in com.example.app
package com.example.app;

import org.apache.commons.lang3.StringUtils;
import com.example.utils.Calculator;

public class Main {
    public static void main(String[] args) {
        String result = StringUtils.capitalize("hello world");
        System.out.println(result);  // 输出 "Hello world"

        Calculator calculator = new Calculator();
        int sum = calculator.add(5, 3);
        System.out.println("Sum: " + sum);  // 输出 "Sum: 8"
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
requires static 的使用

在一些情况下,某些模块可能只在编译时需要,运行时并不依赖它们。这时可以使用 requires static 关键字。这通常用于测试、编译工具或某些开发时工具。

示例:使用 requires static

假设我们有一个工具模块 com.example.tools,它只在开发过程中用于生成代码,但在生产环境中不需要。

  1. 修改 com.example.app 模块的 module-info.java 文件
// module-info.java in com.example.app
module com.example.app {
    requires com.example.utils;
    requires static com.example.tools;  // 静态依赖工具模块,仅在编译时需要
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  1. 使用该模块

由于 com.example.tools 是静态依赖,代码运行时不会引用它,仅在编译时能够使用它的功能。

// Main.java in com.example.app
package com.example.app;

public class Main {
    public static void main(String[] args) {
        // 运行时并不需要 com.example.tools,因此它不会影响最终的运行环境
        System.out.println("App running without static dependencies");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
usesprovides:模块服务机制

Java 9 引入了服务提供者接口(SPI)的概念,允许模块通过 usesprovides 关键字来声明和实现服务。这为 Java 提供了一种灵活的插件机制。

使用 usesprovides
  1. 定义服务接口

首先定义一个服务接口,模块 A 提供这个服务。

// CalculatorService.java in com.example.utils
package com.example.utils;

public interface CalculatorService {
    int add(int a, int b);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  1. 实现服务接口

com.example.utils 模块中,提供 CalculatorService 接口的具体实现:

// SimpleCalculatorService.java in com.example.utils
package com.example.utils;

public class SimpleCalculatorService implements CalculatorService {
    @Override
    public int add(int a, int b) {
        return a + b;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  1. module-info.java 中声明服务

com.example.utils 模块的 module-info.java 文件中声明提供了服务。

// module-info.java in com.example.utils
module com.example.utils {
    exports com.example.utils;
    provides com.example.utils.CalculatorService with com.example.utils.SimpleCalculatorService;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  1. 使用服务提供者

com.example.app 模块中,使用 uses 关键字声明需要使用 CalculatorService 服务。

// module-info.java in com.example.app
module com.example.app {
    requires com.example.utils;
    uses com.example.utils.CalculatorService;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  1. 在代码中查找并使用服务

最后,使用 ServiceLoader 来加载并使用服务:

// Main.java in com.example.app
package com.example.app;

import com.example.utils.CalculatorService;

import java.util.ServiceLoader;

public class Main {
    public static void main(String[] args) {
        // 使用 ServiceLoader 加载服务
        ServiceLoader<CalculatorService> loader = ServiceLoader.load(CalculatorService.class);
        for (CalculatorService service : loader) {
            int result = service.add(5, 3);
            System.out.println("Result from service: " + result);  // 输出 "Result from service: 8"
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

在这个例子中,com.example.app 模块通过 ServiceLoader 动态加载了 com.example.utils 模块提供的 CalculatorService 实现。

模块的版本控制

Java 9 模块化系统允许我们对模块进行版本控制。通过指定模块版本,可以避免版本冲突,确保项目中使用的是兼容版本的模块。

版本控制示例

假设我们有两个版本的 com.example.utils 模块,分别为 1.02.0。在 module-info.java 中,我们可以通过 version 来指定模块版本。

// module-info.java for com.example.utils 1.0
module com.example.utils {
    exports com.example.utils;
    version "1.0";
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

在其他模块中,我们可以根据需要指定依赖的模块版本。

// module-info.java in com.example.app
module com.example.app {
    requires com.example.utils;  // 默认使用最新的版本
    requires [email protected];  // 指定特定版本
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

通过这种方式,Java 9 模块化为版本控制提供了更加灵活的机制,帮助开发者在多版本的依赖中避免冲突。

小结

通过模块化,Java 9 提供了强大的工具来管理代码结构、模块间的依赖关系以及服务发现机制。通过合理使用 requires transitiverequires staticusesprovides,我们能够更加高效地管理复杂的项目。在接下来的章节中,我们将继续探讨如何优化模块的构建和测试过程。

 Java 9 模块化:如何使用模块化提升代码管理_java