模块化系统(JPMS)

Java 9 引入了模块化系统(Java Platform Module System,简称 JPMS),这是 Java 历史上最大的变化之一。

但说实话,这东西挺难理解的。我当年学的时候,看了半天 module-info.java 也不知道怎么写。

今天我们就来把模块化系统彻底讲清楚。

一、为什么需要模块化

1.1 之前的问题:类路径的黑暗时代

在 Java 9 之前,所有类都在类路径(classpath)里:

classpath:
├── myapp.jar
├── lib/
│   ├── spring-core.jar
│   ├── commons-lang.jar
│   └── ...

问题:

  1. JAR Hell:多个版本的同一 JAR 在 classpath
  2. 无封装:任何代码都可以访问 java.lang.reflect 等内部 API
  3. 启动慢:JVM 启动要加载所有类,不知道模块依赖

1.2 模块化的目标

┌─────────────────────────────────────────┐
│              Java Platform               │
├─────────────────────────────────────────┤
│  ┌─────────┐  ┌─────────┐  ┌─────────┐ │
│  │ java.base│ │java.sql│  │java.xml │ │
│  └─────────┘  └─────────┘  └─────────┘ │
│       │           │           │        │
│       └───────────┴───────────┘        │
│                  │                     │
│              显式依赖声明                │
│                                         │
│  ┌─────────────────────────────────────┐│
│  │             myapp.jar               ││
│  │            requires java.sql          ││
│  └─────────────────────────────────────┘│
└─────────────────────────────────────────┘

模块化的三个目标:

  1. 可靠的配置:显式声明模块依赖
  2. 强封装:隐藏内部实现,只暴露公开 API
  3. 可扩展:应用可以扩展平台模块

二、module-info.java 的基本结构

2.1 语法

// module-info.java
module 模块名 {
    // 依赖声明
    requires 模块名;
    
    // 导出声明
    exports 包名;
    
    // 其他指令
}

2.2 简单示例

// 创建一个叫 com.myapp 的模块
module com.myapp {
    // 依赖其他模块
    requires java.sql;
    requires org.springframework.core;
    
    // 导出包(让其他模块可以使用)
    exports com.myapp.service;
    exports com.myapp.controller;
}

三、核心关键字

3.1 requires:声明依赖

module com.myapp {
    // 依赖某个模块
    requires java.sql;
    
    // 依赖某个模块,并指定传递性(transitive)
    requires transitive org.springframework.beans;
    
    // static:编译时必须,运行时不强制
    requires static org.slf4j;  // 只在编译时需要
}
关键字说明
requires必须依赖
requires static编译时必须,运行时可选
requires transitive依赖传递,依赖我的模块也会自动依赖这个模块

3.2 exports:导出包

module com.myapp {
    // 导出某个包,让其他模块可以使用
    exports com.myapp.service;
    exports com.myapp.controller;
    
    // qualified exports:只导出给特定模块
    exports com.myapp.internal to com.othermodule;
}
关键字说明
exports导出给所有模块
exports ... to ...只导出给指定模块(qualified export)

3.3 uses 和 provides:服务接口

// ServiceLocator 是服务接口
module com.myapp {
    // 使用某个服务
    uses com.myapp.spi.MessageService;
    
    // 提供服务的实现
    provides com.myapp.spi.MessageService 
        with com.myapp.impl.EmailService, com.myapp.impl.SmsService;
}

这允许模块定义接口,实现类在另一个模块,运行时动态加载。

四、内置模块

4.1 查看可用模块

java --list-modules

4.2 常用模块

模块名说明
java.base基础模块,包含 Object、String 等
java.sqlJDBC
java.xmlXML 处理
java.desktopGUI
java.logging日志
JDK 内部模块
jdk.compilerJava 编译器
jdk.jshellREPL

五、【直观类比】

【直观类比】

模块化就像一个公司的部门:

模块 = 部门

exports = 对外公布的业务接口
         (市场部公布的业务流程)

requires = 需要的支持部门
         (需要财务部支持)

uses/provides = 外包服务
              (IT 部使用外包公司,外包公司提供服务)

每个部门只知道自己需要什么部门,不需要知道内部细节。

六、模块路径 vs 类路径

6.1 区别

维度类路径模块路径
依赖声明没有显式 requires
访问控制public 都可以访问exports 才是公开的
JAR 分类没有区分普通 JAR vs 模块化 JAR
启动快(按需加载)

6.2 unnamed module 和 automatic module

类型说明来源
unnamed module类路径上的 JAR老 JAR
automatic module类路径上的 JAR 自动成为模块临时方案
正式 modulemodule-info.java迁移目标
# 自动模块:JAR 文件名去掉版本号就是模块名
spring-core-5.3.0.jar 模块名 spring.core

七、生产避坑

7.1 ❌ 错误示范:导出太多包

// ❌ 导出所有包
module com.myapp {
    exports com.myapp;  // 整个包都导出,包括内部类
}

正确做法:

// ✅ 只导出公开 API
module com.myapp {
    exports com.myapp.service;  // 只导出服务接口
    // 不导出 com.myapp.internal
}

7.2 ❌ 错误示范:忘记 transitive

module com.myapp {
    requires org.springframework.beans;  // 你的模块用了 spring.beans
}

// 但如果另一个模块依赖你的模块,它可能无法访问 spring.beans!

// ✅ 应该加上 transitive
module com.myapp {
    requires transitive org.springframework.beans;
}

7.3 ❌ 错误示范:访问未导出的模块

// 在 com.myapp.internal 里
class InternalService {
    // 这个类没有导出
}

// 从另一个模块访问
// ❌ 编译错误:模块无法访问
InternalService service = new InternalService();

正确做法:让模块显式导出:

module com.myapp {
    exports com.myapp.internal to com.othermodule;  // 导出给指定模块
}

八、面试追问链

第一层:基础概念

面试官问:"什么是 Java 模块化?"

Java 9 引入的模块系统,通过 module-info.java 声明模块名、依赖、导出等。可以显式声明模块间的依赖关系,提供更强的封装性。

第二层:关键字

面试官问:"requires 和 exports 有什么区别?"

requires 声明依赖的模块,exports 声明导出的包。requires 告诉 JVM "我需要这个模块才能编译/运行",exports 告诉 JVM "这个包可以被其他模块访问"。

第三层:迁移

面试官问:"如何从老项目迁移到模块化?"

可以先用 automatic module 方式,让老 JAR 自动成为模块。然后逐步给每个模块添加 module-info.java,显式声明依赖和导出。

【学习小结】

  • 模块化通过 module-info.java 声明
  • requires 声明依赖,exports 导出包
  • uses/provides 实现服务发现
  • 类路径上的老 JAR 是 unnamed module
  • 模块路径提供更强的封装性
  • 可以逐步迁移到模块化