Skip to content

模块系统

JDK 9 之前,Java 应用是一个 JAR 包加另一个 JAR 包。随着应用变大,依赖变得混乱——不知道哪些类是真的需要的,哪些是遗留的。

JDK 9 引入了模块系统(Java Platform Module System, JPMS),让 Java 第一次有了官方的代码封装和依赖管理机制。

为什么需要模块

传统 JAR 的问题

bash
# 你的应用
app.jar
├── com.myapp.Main.class
├── com.myapp.service.class
└── com.myapp.util.class

# 依赖
lib/
├── spring-core.jar
├── hibernate.jar
├── commons-lang.jar
└── ...(几十个 JAR)

问题来了:

  • JAR 地狱:版本冲突,同一个类出现在多个 JAR 里
  • 无显式依赖:用反射加载类,运行时才知道缺什么
  • 暴露所有类public 就是公开的,无法区分"给内部用"还是"给外部用"
  • 胖镜像:JDK 的 rt.jar 包含所有类,应用只用到 10%,也要全加载

模块化之后

java
// module-info.java
module com.myapp {
    // 显式声明依赖
    requires spring.core;
    requires hibernate.core;
    
    // 显式声明导出
    exports com.myapp.api;      // 这些包对外可见
    exports com.myapp.service;
    
    // 不导出:com.myapp.internal 和 com.myapp.util 是私有的
}

模块定义文件

每个模块根目录下放 module-info.java

java
module com.myapp {
    // 我需要这些模块
    requires java.sql;
    requires org.apache.commons.lang3;
    
    // 我的这些包对外可见
    exports com.myapp.api;
    exports com.myapp.service;
    exports com.myapp.model;
    
    // 我的这些包只给特定模块用
    exports com.myapp.internal to com.myapp.web;
    
    // 允许反射访问(用于框架)
    opens com.myapp.model;
    opens com.myapp.service to spring.core, hibernate.core;
}

关键字解释

关键字作用
module声明一个模块
requires声明依赖的模块
requires static编译时需要,运行时不需要(可选依赖)
exports导出包,使其对其他模块可见
exports ... to ...定向导出,只给指定模块用
opens允许反射访问(运行时)
opens ... to ...定向开放反射
provides ... with服务提供(ServiceLoader)
uses使用服务(ServiceLoader)

创建模块项目

目录结构

myapp/
├── src/
│   └── com.myapp/          # 模块源代码
│       ├── module-info.java
│       └── com/
│           └── myapp/
│               ├── Main.java
│               └── service/
│                   └── UserService.java
├── lib/                    # 依赖的 JAR
└── module-path/            # 编译后的模块

示例代码

java
// module-info.java
module com.myapp {
    requires java.sql;
    
    exports com.myapp.service;
    exports com.myapp.model;
}

// com/myapp/model/User.java
package com.myapp.model;

public record User(Long id, String name) {}

// com/myapp/service/UserService.java
package com.myapp.service;

import com.myapp.model.User;
import java.util.List;

public class UserService {
    public List<User> findAll() {
        return List.of(
            new User(1L, "Alice"),
            new User(2L, "Bob")
        );
    }
}

// com/myapp/Main.java
package com.myapp;

import com.myapp.service.UserService;

public class Main {
    public static void main(String[] args) {
        UserService service = new UserService();
        service.findAll().forEach(System.out::println);
    }
}

编译和运行

bash
# 编译模块
$ javac -d out --module-source-path src \
    $(find src -name "*.java")

# 运行模块
$ java --module-path out \
    -m com.myapp/com.myapp.Main

JDK 内置模块

JDK 本身也被划分成了模块:

bash
# 列出所有可用模块
$ java --list-modules

java.base@21
java.compiler@21
java.datatransfer@21
java.desktop@21
java.instrument@21
java.logging@21
java.management@21
java.naming@21
java.net.http@21
java.prefs@21
...

常用模块:

模块说明
java.base基础类(所有模块隐式依赖)
java.sqlJDBC
java.namingJNDI
java.desktopAWT、Swing
java.net.httpHttpClient
java.xmlXML 处理
jdk.compilerJavac 编译器

访问控制增强

模块系统带来了新的访问级别:

java
// 在模块内
module com.mylib {
    exports com.mylib.api;
    // com.mylib.internal 不会被导出
}

// 在模块内可以访问任何 public
com.mylib.internal.SomeClass // ✅ 可以,因为同模块

// 跨模块只有导出的才能访问
com.mylib.api.PublicClass    // ✅ 可以,因为导出了
com.mylib.internal.SomeClass  // ❌ 编译错误,因为没导出

常见问题

问题一:模块找不到

Error: Module XXX not found

解决:确保模块在 --module-path 里,并且 module-info.java 正确。

问题二:包导出冲突

Error: Package XXX is exported from both...

解决:一个包只能属于一个模块。如果多个 JAR 都包含同一个包,不能同时加载。

问题三:反射访问被拒绝

IllegalAccessError: class cannot access class...

解决:用 opensopens ... to 开放反射:

java
module com.myapp {
    // 开放给所有模块
    opens com.myapp.model;
    
    // 只开放给特定模块
    opens com.myapp.service to spring.core, hibernate.core;
}

自动模块

放在模块路径上的传统 JAR,会变成自动模块

bash
# lib/my-legacy.jar 没有 module-info.java
# 放到模块路径后,自动成为模块
$ java --module-path lib:out -m com.myapp

自动模块的特点:

  • 自动 requires 所有其他自动模块
  • 导出所有 public
  • 不允许反射访问未导出的包

小结

模块系统是 JDK 9 最重要的新特性之一:

概念说明
module-info.java模块定义文件
requires声明依赖
exports导出包
opens开放反射
自动模块没有 module-info 的 JAR

不过说实话:大多数中小型项目不需要模块系统。如果你不需要严格的封装和显式依赖,普通的 Maven/Gradle 项目就够用了。

模块系统更适合:

  • 大型企业级应用
  • 需要严格封装内部 API
  • 构建自定义 JDK 镜像(jlink)
  • 图书馆/框架作者

记住:模块系统是可选的,不是强制使用的。

基于 VitePress 构建