模块系统
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.MainJDK 内置模块
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.sql | JDBC |
java.naming | JNDI |
java.desktop | AWT、Swing |
java.net.http | HttpClient |
java.xml | XML 处理 |
jdk.compiler | Javac 编译器 |
访问控制增强
模块系统带来了新的访问级别:
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...解决:用 opens 或 opens ... 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)
- 图书馆/框架作者
记住:模块系统是可选的,不是强制使用的。
