Java 模块化指南

更新于 2025-12-27

Christopher Franklin 2024-06-11

1. 概述

Java 9 引入了一种位于包之上的新抽象层级,正式名称为 Java 平台模块系统(JPMS),简称“模块”(Modules)。

在本教程中,我们将深入探讨这一新系统,并详细讲解其各个方面。

我们还将构建一个简单的项目,以演示本指南中所学的所有概念。


2. 什么是模块?

首先,在学习如何使用模块之前,我们需要理解“模块”到底是什么。

模块是一组紧密相关的包和资源,外加一个新的模块描述文件(module descriptor)。

换句话说,它是一种“包的包”(package of packages)的抽象机制,使我们的代码更具可重用性。

2.1 包(Packages)

模块内部的包与 Java 自诞生以来我们一直使用的 Java 包完全相同。

创建模块时,我们仍然像以往一样将代码组织在包中。

除了组织代码之外,包还用于决定哪些代码可以从模块外部公开访问。我们将在后文详细讨论这一点。

2.2 资源(Resources)

每个模块负责管理自己的资源,如媒体文件或配置文件。

过去,我们会将所有资源放在项目的根目录下,并手动管理哪些资源属于应用程序的哪一部分。

使用模块后,我们可以将所需的图像、XML 文件等与需要它们的模块一起打包,从而让项目更易于管理。

2.3 模块描述符(Module Descriptor)

创建模块时,我们需要包含一个描述文件,用于定义模块的多个方面:

  • 名称(Name):模块的名称
  • 依赖项(Dependencies):该模块所依赖的其他模块列表
  • 公开包(Public Packages):希望对外暴露的包列表
  • 提供的服务(Services Offered):可被其他模块消费的服务实现
  • 使用的服务(Services Consumed):当前模块作为服务消费者
  • 反射权限(Reflection Permissions):显式允许其他类通过反射访问本模块包中的私有成员

模块命名规则类似于包命名(允许使用点号 .,不允许使用连字符 -)。通常采用两种风格:项目风格(如 my.module)或 反向 DNS 风格(如 com.baeldung.mymodule)。本指南将使用项目风格。

注意:我们必须显式列出所有希望公开的包,因为默认情况下所有包都是模块私有的。
同样,默认情况下,我们不能对从其他模块导入的类使用反射。

后文将通过示例展示如何使用模块描述文件。

2.4 模块类型

新模块系统中有四种类型的模块:

  • 系统模块(System Modules):运行 java --list-modules 命令时列出的模块,包括 Java SE 和 JDK 模块。
  • 应用模块(Application Modules):我们通常构建的模块。它们具有明确的名称,并在编译后的 JAR 中包含 module-info.class 文件。
  • 自动模块(Automatic Modules):将现有的 JAR 文件添加到模块路径(module path)即可作为非正式模块使用。模块名由 JAR 文件名派生而来。自动模块可以读取路径中加载的所有其他模块。
  • 未命名模块(Unnamed Module):当类或 JAR 被加载到 classpath(而非 module path)时,会自动加入未命名模块。这是为了向后兼容旧版 Java 代码而设计的“兜底”模块。

2.5 分发方式

模块可以通过两种方式分发:JAR 文件或“展开”的编译项目(exploded compiled project)。这与传统 Java 项目一致。

我们可以创建包含“主应用程序”和多个库模块的多模块项目。

但需要注意:每个 JAR 文件只能包含一个模块

因此,在配置构建文件时,必须确保项目中的每个模块都打包为独立的 JAR。


3. 默认模块

安装 Java 9 后,你会发现 JDK 的结构发生了变化——所有原有包都被迁移到了新的模块系统中。

可通过以下命令查看这些模块:

java --list-modules

这些模块分为四大类:javajavafxjdkoracle

  • java 模块:实现 Java SE 语言规范的核心类。
  • javafx 模块:FX 用户界面库。
  • jdk 模块:JDK 自身所需的组件。
  • oracle 模块:Oracle 特定的功能。

4. 模块声明

要设置一个模块,需在包根目录下创建一个名为 module-info.java 的特殊文件。

该文件即模块描述符,包含构建和使用模块所需的所有信息。

模块声明的基本形式如下:

module myModuleName {
    // 所有指令均为可选
}

module 关键字开头,后接模块名称。

虽然空声明也能工作,但通常我们需要更多配置——这就需要用到模块指令(module directives)。

4.1 requires

requires 指令用于声明模块依赖:

module my.module {
    requires module.name;
}

此时,my.module 在编译期和运行期都依赖 module.name,并且可以访问该依赖导出的所有公共类型。

4.2 requires static

有时我们编写引用其他模块的代码,但用户可能并不需要该功能。

例如,我们可能写了一个工具函数,仅在存在某个日志模块时才美化输出内部状态。但并非所有用户都需要此功能,也不想引入额外的日志库。

此时应使用可选依赖requires static 创建仅在编译期存在的依赖:

module my.module {
    requires static module.name;
}

4.3 requires transitive

当我们使用第三方库时,下游用户也必须引入这些“传递性”依赖,否则无法正常工作。

使用 requires transitive 可强制下游模块自动读取我们的依赖:

module my.module {
    requires transitive module.name;
}

这样,当开发者声明 requires my.module 时,无需再显式声明 requires module.name

4.4 exports

默认情况下,模块不会向其他模块暴露任何 API。强封装性(strong encapsulation)正是模块系统的核心设计目标之一。

虽然安全性更高,但若希望 API 可被外部使用,就必须显式开放。

使用 exports 指令可导出指定包中的所有公共成员:

module my.module {
    exports com.my.package.name;
}

此时,任何 requires my.module 的模块都能访问 com.my.package.name 中的公共类型,但无法访问其他包。

4.5 exports … to

如果不想向全世界开放 API,可使用 exports … to 限制访问权限:

module my.module {
    exports com.my.package.name to com.specific.module;
}

只有 com.specific.module 能访问该包。

4.6 uses

服务(service)是实现特定接口或抽象类的对象,可被其他类消费。

使用 uses 指令声明本模块消费的服务(注意:填写的是接口或抽象类,而非实现类):

module my.module {
    uses class.name;
}

注意:requiresuses 有区别。
我们可能依赖一个提供服务的模块,但该服务实现的接口来自其传递依赖。
使用 uses 可避免强制要求所有传递依赖。

4.7 provides … with

模块也可以作为服务提供者

  • provides 后接接口或抽象类名
  • with 后接具体实现类名
module my.module {
    provides MyInterface with MyInterfaceImpl;
}

4.8 open

Java 9 之前,反射可访问包内所有类型和成员(包括 private),导致封装性形同虚设。

模块系统引入强封装后,必须显式授权才能反射访问。

若希望完全开放反射权限(类似旧版 Java),可使用 open

open module my.module {
}

4.9 opens

若只想开放特定包的反射权限(而非整个模块),使用 opens

module my.module {
    opens com.my.package;
}

注意:这会向所有模块开放该包的反射权限。

4.10 opens … to

若希望仅对特定模块开放反射权限,使用 opens … to

module my.module {
    opens com.my.package to moduleOne, moduleTwo;
}

5. 命令行选项

尽管 Maven 和 Gradle 已支持 Java 9 模块,了解命令行用法仍很有价值。

以下是在完整示例中会用到的关键选项:

  • --module-path:指定模块路径(包含模块的目录列表)
  • --add-reads:命令行版 requires
  • --add-exports:命令行版 exports
  • --add-opens:命令行版 opens
  • --add-modules:将指定模块加入默认根模块集
  • --list-modules:列出所有模块及其版本
  • --patch-module:向模块中添加或覆盖类
  • --illegal-access=permit|warn|deny:控制非法访问行为(默认 permit

6. 可见性(Visibility)

许多库(如 JUnit、Spring)依赖反射实现功能。

Java 9 默认仅允许访问已导出包中的 public 成员。即使调用 setAccessible(true),也无法访问非 public 成员。

可通过 openopensopens…to 授予运行时反射权限(注意:仅限运行时!无法用于编译)。

若需反射访问非自有模块,且无法修改其 module-info.java,可使用命令行 --add-opens

java --add-opens other.module/com.package=MY_MODULE ...

前提:你有权修改运行时的命令行参数。


7. 实战:构建一个模块化项目

现在,我们将通过一个简单项目演示上述所有概念。

为简化操作,不使用 Maven/Gradle,仅用命令行工具。

7.1 项目结构搭建

mkdir module-project
cd module-project
mkdir simple-modules

最终结构如下:

module-project
├── simple-modules
│   ├── hello.modules
│   │   └── com/baeldung/modules/hello/
│   └── main.app
│       └── com/baeldung/modules/main/

7.2 第一个模块:hello.modules

创建 HelloModules.java

// simple-modules/hello.modules/com/baeldung/modules/hello/HelloModules.java
package com.baeldung.modules.hello;

public class HelloModules {
    public static void doSomething() {
        System.out.println("Hello, Modules!");
    }
}

创建模块描述文件:

// simple-modules/hello.modules/module-info.java
module hello.modules {
    exports com.baeldung.modules.hello;
}

7.3 第二个模块:main.app

模块描述:

// simple-modules/main.app/module-info.java
module main.app {
    requires hello.modules;
}

主类:

// simple-modules/main.app/com/baeldung/modules/main/MainApp.java
package com.baeldung.modules.main;

import com.baeldung.modules.hello.HelloModules;

public class MainApp {
    public static void main(String[] args) {
        HelloModules.doSomething();
    }
}

7.4 编译模块

创建 compile-simple-modules.sh

#!/usr/bin/env bash
javac -d outDir --module-source-path simple-modules $(find simple-modules -name "*.java")

执行后,outDir 目录将包含两个编译好的模块。

7.5 运行程序

创建 run-simple-module-app.sh

#!/usr/bin/env bash
java --module-path outDir -m main.app/com.baeldung.modules.main.MainApp

输出:

Hello, Modules!

7.6 添加服务(Service)

hello.modules 中新增接口:

// HelloInterface.java
public interface HelloInterface {
    void sayHello();
}

修改 HelloModules 实现该接口:

public class HelloModules implements HelloInterface {
    public static void doSomething() {
        System.out.println("Hello, Modules!");
    }

    public void sayHello() {
        System.out.println("Hello!");
    }
}

更新 hello.modules 的模块描述:

module hello.modules {
    exports com.baeldung.modules.hello;
    provides com.baeldung.modules.hello.HelloInterface 
        with com.baeldung.modules.hello.HelloModules;
}

main.app 中声明使用服务:

module main.app {
    requires hello.modules;
    uses com.baeldung.modules.hello.HelloInterface;
}

修改主类:

public class MainApp {
    public static void main(String[] args) {
        HelloModules.doSomething();

        Iterable<HelloInterface> services = ServiceLoader.load(HelloInterface.class);
        HelloInterface service = services.iterator().next();
        service.sayHello();
    }
}

重新编译运行,输出:

Hello, Modules!
Hello!

8. 向未命名模块添加模块

未命名模块类似于默认包,不是真正的模块,而是“兜底”容器。

若类不属于任何命名模块,则自动归入未命名模块。

有时需将特定平台/库模块加入默认根模块集(例如运行 Java 8 程序时)。

使用 --add-modules

--add-modules java.xml.bind

Maven 配置示例:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.0</version>
    <configuration>
        <source>9</source>
        <target>9</target>
        <compilerArgs>
            <arg>--add-modules</arg>
            <arg>java.xml.bind</arg>
        </compilerArgs>
    </configuration>
</plugin>

9. 结论

本指南全面介绍了 Java 9 模块系统的基础知识:

  • 定义了模块的概念
  • 展示了 JDK 内置模块的发现方式
  • 详解了模块描述文件的各种指令
  • 介绍了关键命令行参数
  • 通过实战项目巩固了理论知识

模块系统通过强封装和显式依赖,显著提升了 Java 应用的可维护性、安全性和性能。建议开发者在新项目中积极采用,并逐步迁移旧项目。