Java 模块系统概述

Java 模块系统(Java Platform Module System,简称 JPMS)是 Java 9(2017 年发布)引入的一项重大特性,也被称为 Project Jigsaw。该系统旨在解决 Java 早期版本中类路径(classpath)管理的痛点,如 JAR Hell(依赖冲突)、模块化不足和封装性弱等问题。通过模块化,Java 应用可以更好地组织代码、提高安全性、减少依赖并提升性能。下面我将详细解释 Java 模块的核心概念、语法、用法以及优势与注意事项。

1. 什么是 Java 模块?

  • 基本定义:模块(Module)是 Java 中一种新的代码组织单位,它是一组相关包(packages)的集合,加上一个模块描述文件(module-info.java)。模块可以明确定义其依赖的其他模块、导出的包(exposed packages),以及对反射的访问权限。
  • 与包(Package)的区别:包是命名空间,用于组织类;模块是包的更高层抽象,用于组织应用或库的整体结构。模块可以包含多个包,但一个包只能属于一个模块。
  • 模块类型
    • 命名模块(Named Module):有明确名称的模块,通常由开发者定义。
    • 自动模块(Automatic Module):将旧的 JAR 文件作为模块使用时自动生成的模块,名称基于 JAR 文件名(去掉版本号和扩展名)。
    • 未命名模块(Unnamed Module):所有类路径上的类默认属于未命名模块,不能被其他模块依赖。
  • 模块路径(Module Path):类似于类路径,但专用于模块化的 JAR 文件。编译和运行时使用 --module-path-p 选项指定。

2. 如何定义一个模块?

模块的核心是 module-info.java 文件,它位于模块的根目录下,必须是源代码的一部分。以下是其基本语法:

module module.name {  // 模块名称,通常是小写字母和点分隔,如 com.example.mymodule
    // 依赖声明
    requires other.module;  // 依赖其他模块(transitive 表示传递依赖)
    
    // 导出包
    exports com.example.package;  // 导出包给所有模块使用
    exports com.example.internal to friend.module;  // 限定导出,只给特定模块使用
    
    // 打开包(用于反射)
    opens com.example.reflection;  // 允许运行时反射访问
    
    // 服务提供与使用
    provides com.example.Service with com.example.Impl;  // 提供服务实现
    uses com.example.Service;  // 使用服务接口
    
    // 其他(可选)
    requires static another.module;  // 编译时依赖,运行时可选
}
  • 关键指令
    • requires:声明依赖的模块。如果加上 transitive,则依赖是传递的(被依赖的模块也会被下游模块看到)。
    • exports:导出包中的公共类型(public classes/interfaces),允许其他模块访问。未导出的包是内部的,无法从外部访问。
    • opens:允许反射访问包中的类型(即使是非公共的)。在 Java 9+ 中,反射默认受限,需要显式打开。
    • provides ... with:注册服务实现(Service Provider Interface, SPI)。
    • uses:声明使用某个服务接口。
  • 模块名称规则:建议使用反向域名(如 com.example.mymodule),避免冲突。

3. 模块的使用流程

  • 创建模块
    1. 在项目中创建 src/module.name 目录结构。
    2. 在根目录下添加 module-info.java
    3. 编写代码,确保包结构匹配导出声明。
  • 编译模块
    使用 javac 命令:
    javac -d mods/module.name --module-source-path src $(find src -name "*.java")
    这会生成模块化的 .class 文件。
  • 打包模块
    使用 jar 命令创建模块化 JAR(Modular JAR):
    jar --create --file mlib/module.name.jar --module-version 1.0 -C mods/module.name .
  • 运行模块
    使用 java 命令指定主模块:
    java --module-path mlib -m module.name/com.example.MainClass
  • 多模块项目:在 Maven 或 Gradle 中支持模块化。例如,在 Maven 的 pom.xml 中添加 <module>module.name</module>,并使用 multi-module 项目结构。

4. 模块的优势

使用表格总结模块系统的关键优势与传统 classpath 的比较:

方面传统 Classpath模块系统 (JPMS)
封装性弱,所有 public 类型全局可见强,只有导出的包可见
依赖管理容易冲突(JAR Hell)显式依赖,减少冲突
安全性反射无限制默认限制反射,需显式 opens
性能启动慢,加载所有类更小镜像(jlink),更快启动
可维护性代码组织松散模块边界清晰,便于大型项目
兼容性旧代码直接兼容支持自动模块过渡旧 JAR
  • jlink 工具:用于创建自定义运行时镜像,只包含所需模块,显著减小 JDK 体积(从数百 MB 到数十 MB)。
  • 服务加载:模块化增强了 ServiceLoader,允许更精确的服务发现。

5. 常见问题与注意事项

  • 兼容旧代码:Java 9+ 兼容旧 JAR,但推荐逐步迁移。使用 --add-modules 添加模块,或 --add-exports 临时导出包。
  • 反射限制:许多库(如 Spring、Hibernate)依赖反射,在模块中使用时需添加 --add-opens 或在 module-info 中 opens 包。
  • 版本问题:模块不支持版本化依赖(不像 Maven),依赖名称唯一。
  • 分模块 JDK:JDK 本身被拆分成模块(如 java.base、java.sql),应用只需 requires 所需模块。
  • 潜在挑战
    • 迁移成本高:大型遗留项目需重构 module-info。
    • 第三方库兼容:不是所有库都模块化,使用自动模块作为桥接。
    • 测试:使用 JUnit 时,确保测试模块 requires junit。
  • 最新发展(截至 2025 年):Java 模块系统在 Java 21+ 中进一步优化,如支持更细粒度的反射控制和更好的工具集成(例如 jpackage 用于打包模块化应用)。在云原生环境中,模块化有助于创建更小的 Docker 镜像。

6. 示例:一个简单的模块化应用

假设有两个模块:com.example.logger(提供日志服务)和 com.example.app(主应用)。

  • com.example.logger/module-info.java

    module com.example.logger {
        exports com.example.logger;
    }
  • com.example.app/module-info.java

    module com.example.app {
        requires com.example.logger;
    }
  • 主类 com.example.app.Main

    package com.example.app;
    import com.example.logger.Logger;
    public class Main {
        public static void main(String[] args) {
            Logger.log("Hello, Modules!");
        }
    }

编译、打包后运行,即可看到模块化效果。