1. 引言
1.1 什么是 Project Jigsaw?
Project Jigsaw 是 Java 9 引入的一项重要特性,其核心是将 Java 平台引入模块化系统。这项特性最早由 Oracle 于 JSR 376 提出,旨在解决 Java 平台和大型应用程序架构中的一系列结构性问题。模块系统是对 Java 类加载器机制和访问控制模型的系统性扩展,它不仅影响开发者编写代码的方式,还改变了平台的打包、部署和运行方式。
简而言之,Project Jigsaw 为 Java 引入了新的构建单元——模块(module),用于组织代码和资源,并显式声明依赖关系和可访问性边界。
Java 模块系统的核心组成部分是模块描述文件 module-info.java
,它定义了模块对外公开的包、依赖的其他模块、使用的服务等。这种方式替代了传统的隐式依赖、扁平类路径模式,使得系统的边界更清晰、耦合度更低、启动更高效。
1.2 模块化系统的好处
引入模块系统之后,Java 平台和开发者可以享受以下诸多优势:
更强的封装性
模块系统允许模块显式声明哪些包对外公开(exports
),哪些包是模块内部的实现细节。这极大提升了封装性,避免了以往所有类都能被任意访问的局面。
更清晰的依赖管理
通过 requires
语句,模块可以声明依赖的其他模块,Java 编译器和运行时可以根据这些信息检测缺失依赖、冲突、循环依赖等问题,从而提升构建的健壮性。
启动时间更快、运行效率更高
JVM 可以基于模块信息进行更精准的类加载和优化,从而提升启动时间和运行性能。JLink 等工具也可以基于模块化信息生成更小巧的运行时镜像。
安全性提升
模块强封装机制能够有效防止反射访问内部类,提高系统的安全性。此外,只有明确导出的包才对外开放,也避免了非预期访问。
可维护性与可演进性增强
模块系统天然支持系统拆分、边界划分,代码结构更清晰,有利于团队协作、功能演进和代码重构。
支持定制运行时
使用工具如 jlink
,开发者可以按需定制 Java 运行时,仅包含需要的模块,构建最小化、轻量化的专用运行环境。
综上所述,Project Jigsaw 不只是语法糖或语义扩展,而是 Java 平台结构层面的根本性变革。
2. 背景与历史
2.1 Java 的演化与模块化需求
在 Java 9 之前,Java 平台主要通过类、包、JAR 文件进行组织。虽然这些结构在最初满足了中小型项目的需求,但随着应用程序和平台本身的规模不断扩大,现有结构显现出诸多局限:
-
类路径污染:Java 传统的类路径(classpath)是一种扁平结构,所有类都在一个共享的命名空间中,容易出现类冲突(ClassNotFoundException 或 ClassCastException)。
-
缺乏明确依赖声明:在类路径中无法明确指定模块之间的依赖关系,只能通过手工维护文档或构建脚本处理。
-
难以隐藏内部实现:包级别的访问控制无法阻止其他代码通过反射访问内部类,封装性不足。
-
难以拆分平台:JDK 是一个整体,无法裁剪无关模块,导致部署包庞大,难以适配轻量级设备或云原生场景。
-
工具链支持受限:由于缺乏结构化信息,像编译器、IDE、构建工具难以准确判断依赖和范围,影响智能提示、重构、安全性。
这些问题催生了对更高级别的结构化支持,也为 Project Jigsaw 的提出奠定了基础。
2.2 Project Jigsaw 的设计目标
Project Jigsaw 并不仅仅是引入“模块”这一语法概念,而是旨在从语言、平台、工具三方面彻底改进 Java 的模块化能力,其核心目标包括:
1. 可伸缩的模块平台(Scalable Platform)
将 JDK 拆分为若干个标准模块,使其能够按需组合、精简打包,以适配嵌入式、容器化部署等轻量化场景。例如,Java 9 开始,JDK 被拆分为大约 90 个模块(如 java.base、java.sql、java.logging 等)。
2. 增强的封装性(Stronger Encapsulation)
通过模块描述符(module-info.java
)显式定义模块公开的 API 和隐藏的实现细节,防止外部代码滥用内部类,提升封装性和安全性。
3. 可靠的配置机制(Reliable Configuration)
通过模块系统显式声明依赖(requires
)和导出(exports
),在编译和运行时自动检测缺失依赖、重复依赖或循环依赖,从而构建更加健壮的系统。
4. 多语言支持(Multiple Language Support)
支持用不同语言(如 Java、Kotlin、Scala)编写的模块共存于同一应用中,只要遵循模块结构即可集成。
5. 兼容历史系统(Maintain Backward Compatibility)
兼容现有非模块化的代码和库,支持逐步迁移;引入“自动模块”“未命名模块”等机制为过渡提供缓冲。
Project Jigsaw 经过多个版本的迭代,最终在 Java 9 正式合并入主线分支(JEP 261),成为该版本的核心特性之一。
3. 理解模块
Java 9 模块化系统的核心概念是“模块(module)”。本章将从模块的语义、结构、语法等维度详细解析,帮助读者建立起对 Java 模块的系统性理解。
3.1 模块的定义与基本结构
在 Java 中,模块是一组相关包(packages)和资源的集合。它们打包在一起形成应用、库或运行时的一部分。模块通过一个特殊的描述文件 module-info.java
显式地声明它的依赖、暴露的 API、使用的服务等信息。
模块的基本特征:
-
自包含(Self-contained):每个模块包含自己需要的类和资源。
-
显式依赖(Explicit dependencies):使用
requires
语句声明依赖其他模块。 -
受控访问(Controlled access):通过
exports
控制对外暴露的包,其它包保持封装。 -
服务声明(Service declaration):模块可声明使用/提供服务,便于松耦合编程。
模块与包、类的区别:
级别 | 概念 | 作用 |
---|---|---|
类(Class) | 最小的代码单元 | 实现具体功能逻辑 |
包(Package) | 类的逻辑分组 | 控制类访问范围 |
模块(Module) | 包的逻辑分组 | 控制包的可见性和依赖关系 |
模块是对包和类组织的进一步抽象,用于定义更大粒度的访问边界和依赖管理。
3.2 module-info.java
语法详解
module-info.java
是每个模块的声明文件,位于模块的根目录下。它告诉 Java 编译器和 JVM:
-
本模块叫什么名字
-
本模块依赖哪些其他模块
-
本模块开放哪些包给其他模块
-
本模块使用/提供哪些服务
以下是一个典型示例:
module com.example.app {
requires java.sql;
requires com.example.utils;
exports com.example.app.api;
exports com.example.app.model;
uses com.example.spi.MyService;
provides com.example.spi.MyService with com.example.impl.MyServiceImpl;
}
常见关键字说明:
-
module
:声明模块名,必须唯一,建议使用包名风格(如com.company.module
)。 -
requires
:声明对其他模块的依赖。 -
exports
:声明对外暴露的包。 -
opens
:声明开放包给反射访问(如 JAXB、Spring)。不同于exports
,opens
是运行时开放而非编译期开放。 -
uses
:声明该模块使用某个服务接口(配合ServiceLoader
)。 -
provides ... with
:声明实现了某个服务接口的具体类。
3.3 模块 vs 包 vs 类
为帮助大家更系统性理解模块的抽象层次,我们可以从代码组织结构上进行比较:
维度 | 类(Class) | 包(Package) | 模块(Module) |
最小构成 | 方法、字段 | 类集合 | 包集合 |
封装边界 | private 、protected | 默认访问修饰符 | exports /opens |
单元标识 | 类名 | 包名 | 模块名(module-info.java ) |
可见性控制 | 成员级别 | 类级别 | 包级别 |
依赖声明 | 构造函数/成员变量 | 无 | requires |
示例对比说明:
假设我们开发了一个图书管理系统,其中包含三个模块:
-
com.library.core
:核心功能模块 -
com.library.db
:数据访问模块,依赖core
-
com.library.app
:应用启动模块,依赖core
和db
每个模块都包含若干包,如:
com.library.core.api
com.library.core.impl
com.library.db.dao
com.library.app.main
而模块之间的依赖关系、暴露接口、服务使用就通过 module-info.java
来完成组织与管理。这种结构清晰可控,远胜传统 JAR 混杂的方式。
4. 创建模块化应用程序
本章将通过实际示例,带领你从零构建一个模块化的 Java 应用程序。我们将涵盖模块的结构组织、模块声明的编写、模块之间的依赖配置等操作。内容循序渐进,适合刚接触模块系统的开发者。
4.1 构建基本模块结构
在模块化开发中,项目的目录结构清晰规范是成功的关键。下面是一个典型的多模块项目结构:
bookstore-app/
├── com.bookstore.model/
│ ├── module-info.java
│ └── com/bookstore/model/Book.java
├── com.bookstore.service/
│ ├── module-info.java
│ └── com/bookstore/service/BookService.java
├── com.bookstore.main/
│ ├── module-info.java
│ └── com/bookstore/main/Main.java
模块说明:
-
com.bookstore.model
:包含实体类Book
-
com.bookstore.service
:包含业务逻辑类BookService
,依赖model
模块 -
com.bookstore.main
:应用入口,依赖model
和service
4.2 示例:简单模块化应用程序
1. com.bookstore.model
模块
module-info.java
module com.bookstore.model {
exports com.bookstore.model;
}
Book.java
package com.bookstore.model;
public class Book {
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
public String getTitle() { return title; }
public String getAuthor() { return author; }
@Override
public String toString() {
return title + " by " + author;
}
}
2. com.bookstore.service
模块
module-info.java
module com.bookstore.service {
requires com.bookstore.model;
exports com.bookstore.service;
}
BookService.java
package com.bookstore.service;
import com.bookstore.model.Book;
public class BookService {
public Book getSampleBook() {
return new Book("Effective Java", "Joshua Bloch");
}
}
3. com.bookstore.main
模块
module-info.java
module com.bookstore.main {
requires com.bookstore.model;
requires com.bookstore.service;
}
Main.java
package com.bookstore.main;
import com.bookstore.model.Book;
import com.bookstore.service.BookService;
public class Main {
public static void main(String[] args) {
BookService service = new BookService();
Book book = service.getSampleBook();
System.out.println("Book info: " + book);
}
}
这个示例展示了如何使用模块将应用程序的不同职责划分清楚,并通过 requires
和 exports
建立模块之间的联系。
4.3 多模块间的依赖关系处理
在构建多模块系统时,依赖关系的声明和管理尤为关键。模块间依赖通过 requires
指令声明,同时 exports
决定哪些包可供其他模块访问。
依赖传递(Transitive Dependencies)
当模块 A 依赖模块 B,模块 B 又依赖模块 C 时,默认 A 无法访问 C。但如果 B 使用了 requires transitive
,则 A 也可以访问 C。
示例:
module com.example.b {
requires transitive com.example.c;
}
module com.example.a {
requires com.example.b; // 可隐式访问 com.example.c
}
反射访问问题与 opens
默认情况下,Java 模块不会允许反射访问其内部类。如果使用框架如 Spring、JAXB 等需要反射,可以使用 opens
指令开放包。
opens com.example.model to spring.core;
冲突与重复依赖
模块化系统会在编译期和运行期检测重复模块声明或循环依赖,防止潜在问题。例如:
5. 构建模块化应用程序
在上一章中,我们已经完成了一个简单模块化项目的结构搭建与代码编写。本章将深入讲解如何通过命令行工具对模块化项目进行构建,包括编译、打包、配置模块路径,以及类路径与模块路径的区别。
5.1 使用 javac
编译模块
Java 9 引入了 --module-source-path
和 --module
参数,支持编译模块化源代码。我们仍以上一章中的 bookstore 项目为例,展示如何逐步构建。
1. 项目目录组织(源代码)
bookstore-app/
├── mods/ <-- 编译输出目录
├── src/
│ ├── com.bookstore.model/
│ │ └── ...
│ ├── com.bookstore.service/
│ │ └── ...
│ └── com.bookstore.main/
│ └── ...
2. 编译命令示例
# 编译 model 模块
javac -d mods/com.bookstore.model \
src/com.bookstore.model/module-info.java \
src/com.bookstore.model/com/bookstore/model/*.java
# 编译 service 模块(依赖 model)
javac --module-path mods \
-d mods/com.bookstore.service \
src/com.bookstore.service/module-info.java \
src/com.bookstore.service/com/bookstore/service/*.java
# 编译 main 模块(依赖 model 和 service)
javac --module-path mods \
-d mods/com.bookstore.main \
src/com.bookstore.main/module-info.java \
src/com.bookstore.main/com/bookstore/main/*.java
说明:
-
--module-path mods
:设置模块路径,Javac 会在此目录下查找依赖模块。 -
-d
:指定编译后的输出目录。 -
每个模块编译到 mods 目录下以模块名命名的子目录中。
5.2 模块路径与类路径的区别
模块路径和类路径虽然都用于定位类文件,但它们在模块系统中的作用完全不同:
特性 | 类路径(Classpath) | 模块路径(Module Path) |
---|---|---|
文件结构 | 扁平结构 | 结构化,模块为单位 |
依赖声明 | 隐式依赖(通过代码或构建工具) | 显式依赖(通过 module-info.java ) |
封装控制 | 无 | 严格控制,仅导出包可访问 |
冲突处理 | 类冲突不易发现,可能运行时失败 | 编译/运行期检测冲突 |
注意:模块路径是模块系统工作的核心,不支持传统类路径的类自动访问模块,也不能把模块和非模块混合在类路径中使用。
5.3 创建模块化 JAR 文件
编译完成后,我们通常需要将模块打包成 JAR 文件进行分发或部署。打包时保留模块结构。
jar --create --file=mlibs/com.bookstore.model.jar \
--main-class=com.bookstore.model.Book \
-C mods/com.bookstore.model .
jar --create --file=mlibs/com.bookstore.service.jar \
-C mods/com.bookstore.service .
jar --create --file=mlibs/com.bookstore.main.jar \
--main-class=com.bookstore.main.Main \
-C mods/com.bookstore.main .
说明:
-
--create
:表示创建 JAR -
--file
:指定输出文件名 -
--main-class
:指定 JAR 启动入口(可选) -
-C
:切换到对应目录压缩其内容
通过以上步骤,我们就成功将模块编译并打包为结构化的模块化 JAR 文件,可供运行或发布使用。
6. 运行模块化应用程序
在构建完成模块化应用程序之后,下一步就是运行它。本章将详细讲解如何使用 java
命令启动模块化程序,解析关键参数,如 --module-path
和 --module
,并说明如何设置启动类和调试常见问题。
6.1 使用 java
命令运行模块
Java 9 开始,java
命令支持模块化启动,通过指定模块路径与主模块来运行应用程序。
示例:运行 bookstore 应用
假设我们已将三个模块编译并打包成 JAR 文件,保存在 mlibs/
目录中:
mlibs/
├── com.bookstore.model.jar
├── com.bookstore.service.jar
└── com.bookstore.main.jar
我们希望运行 com.bookstore.main
模块中的 Main
类:
java --module-path mlibs \
--module com.bookstore.main/com.bookstore.main.Main
说明:
-
--module-path mlibs
:指定模块搜索路径(等价于类路径中的-cp
)。 -
--module <module>/<main-class>
:指定要启动的模块及其主类。
注意:如果模块已在 module-info.java
中使用 main-class
指定了主类,也可以简写:
java --module-path mlibs --module com.bookstore.main
6.2 多模块路径与运行时依赖
模块路径可以接受多个目录或 JAR 路径,使用平台分隔符(Unix 为 :
,Windows 为 ;
)分隔。例如:
java --module-path "lib:mlibs" --module my.main/module.Main
同时,模块路径下的每个 JAR 都应是合法的模块化 JAR,即含有 module-info.class
文件。否则只能作为“自动模块”或放入类路径处理。
6.3 启动错误与排查
模块化应用启动常见问题包括:
1. 找不到模块
Error: Module com.example.app not found
排查:确保 --module-path
指向正确目录,并含有目标模块的 JAR。
2. 无法访问类或包
Error: class com.example.Foo is not accessible
排查:检查目标类是否在导出的包中(exports
),或者是否被 opens
打开。
3. 主类未定义
Error: no main manifest attribute, in com.example.app.jar
排查:使用 --module <module>/<MainClass>
方式手动指定主类,或在 module-info.java
中添加 main-class
指令并重新打包。
6.4 在 IDE 中运行模块应用
尽管命令行控制最为灵活,但在实际开发中我们常使用 IDE(如 IntelliJ IDEA、Eclipse)来运行模块化应用。大多数主流 IDE 在 Java 9 之后已原生支持模块结构。
IntelliJ IDEA 示例:
-
新建 Java Module 项目,确保启用 module-info.java。
-
在运行配置中设置
--module-path
和主模块信息。 -
IDEA 会自动处理模块依赖和类路径设置。
Eclipse 示例:
-
安装支持 Java 9+ 的 Eclipse 版本。
-
创建 Modular Project。
-
使用 Project Properties > Java Build Path > Module Dependencies 管理依赖。
7. 迁移现有项目到模块化系统
将一个已有的 Java 应用迁移到 Java 9 模块系统并非易事,尤其对于历史遗留系统而言。本章将指导你逐步完成迁移过程,从分析依赖、处理封装冲突到分阶段迁移策略,助力老项目安全平滑地过渡到模块化架构。
7.1 遗留系统迁移的挑战
遗留项目通常存在以下问题:
-
类路径依赖复杂、重复:多个 JAR 之间存在冲突或循环依赖。
-
广泛使用反射访问内部类:模块化系统默认禁止对未开放包的反射访问。
-
无明确边界定义:所有类在默认包结构下暴露,没有封装控制。
-
缺乏结构化构建管理:如使用 Ant 或手工构建,缺乏模块化编译流程。
这些问题导致无法直接将项目转为模块化系统,必须采取渐进式、工具辅助的方式进行迁移。
7.2 使用 jdeps
工具分析依赖
Java 提供了 jdeps
工具用于分析 JAR 文件之间的依赖关系,帮助识别模块划分、未使用依赖、非法访问等。
示例:分析模块依赖
jdeps --module-path mods -s \
--multi-release 9 \
--generate-module-info out-dir \
my-legacy-lib.jar
说明:
-
--multi-release 9
:为支持多版本类文件的分析 -
--generate-module-info
:自动生成初始的module-info.java
(需手工完善) -
-s
:简略输出依赖(仅模块名)
该工具将帮助我们识别:
-
依赖哪些模块(如
java.xml
,java.sql
, 第三方库) -
哪些包存在非法访问(例如未导出的内部类)
-
哪些 JAR 可以归并、拆分为模块
7.3 分阶段迁移策略与实战示例
迁移路径推荐分为以下几步:
第一步:构建模块结构,保留类路径兼容
将应用拆分为逻辑子模块,并为每个模块建立对应的目录与初始 module-info.java
,但暂时仍使用类路径启动。
第二步:转换主模块为真正模块
选择启动模块(如 app
)作为首个真正模块,编写完整 module-info.java
,指定依赖与导出包。
第三步:使用自动模块过渡依赖
对于暂未模块化的第三方依赖(如旧版 Apache Commons JAR),可以放入模块路径,由 JVM 自动识别为“自动模块”:
jar --create --file=mlibs/legacy.jar -C legacy-lib/ .
java --module-path mlibs --module my.app/com.my.App
JVM 会基于 JAR 文件名生成模块名,但不建议长期依赖自动模块,应尽快替换为显式模块。
第四步:解决强封装与反射访问问题
若应用使用反射访问未导出类,可通过以下方式解决:
-
在
module-info.java
中使用opens
语句开放包:
opens com.example.internal to spring.core;
-
启动时使用命令行参数开放访问(不推荐长期依赖):
--add-opens com.example.internal/com.example.internal.Class=ALL-UNNAMED
第五步:逐步模块化其他模块与依赖
-
对于核心业务模块、工具模块逐一补全
module-info.java
-
替换非模块化第三方依赖为模块化版本(如使用 Maven Central 发布的模块 JAR)
-
使用构建工具如 Maven/Gradle 配合插件(如
moditect
)管理模块边界与打包
8. 高级模块特性
Java 模块系统不仅支持基本的模块定义与依赖管理,还引入了多项高级特性,使模块系统更具扩展性和灵活性。本章将详尽探讨这些特性,包括服务加载机制(uses
与 provides
)、自动模块与未命名模块的作用,以及强封装的相关参数处理方法。
8.1 uses
与 provides
:模块化服务加载机制
在模块系统中,服务的声明与发现不再仅靠 SPI(Service Provider Interface)文件,而是通过 uses
与 provides
指令完成。
uses
:声明模块依赖某服务
module com.example.client {
uses com.example.spi.Formatter;
}
provides
:声明模块提供某服务实现
module com.example.impl {
provides com.example.spi.Formatter with com.example.impl.JsonFormatter;
}
使用 ServiceLoader
ServiceLoader<Formatter> loader = ServiceLoader.load(Formatter.class);
for (Formatter f : loader) {
System.out.println(f.format("hello"));
}
这样,客户端模块无需显式依赖具体实现,只需声明依赖服务接口,模块系统将通过 ServiceLoader
自动查找实现。
好处:
-
松耦合:客户端与实现解耦
-
模块隔离:只需依赖接口模块
-
可扩展:新增实现模块无需修改客户端
8.2 自动模块与未命名模块
Java 为了兼容旧有类库,引入了两种非显式模块机制。
自动模块(Automatic Modules)
-
指传统 JAR 被放置于
--module-path
中,无module-info.class
-
JVM 会根据 JAR 文件名推断模块名,并导出所有包
# 假设放入 JAR: lib/commons-lang3-3.12.0.jar
java --module-path lib --module my.module/Main
注意: 自动模块是临时兼容机制,不具备封装控制和稳定模块名。应尽快迁移为显式模块。
未命名模块(Unnamed Module)
-
位于
classpath
中的类和 JAR 属于 unnamed module -
unnamed module 可以访问模块路径中的所有模块
-
模块路径中的模块默认不能访问 unnamed module
用途: 支持非模块化代码与模块化系统混用,但无法享受模块系统的封装与检测能力。
8.3 强封装与参数处理:--add-exports
、--add-opens
模块系统默认只允许访问显式导出的包。如果访问未导出的内部类或方法,需要使用以下参数:
--add-exports
强制将模块的某个包导出给指定模块。
--add-exports java.base/sun.security.util=ALL-UNNAMED
适用于在编译期/运行期访问未导出 API,但并不会开放反射访问。
--add-opens
除了导出包,还允许反射访问(适用于框架如 Spring、JAXB)。
--add-opens java.base/java.lang=ALL-UNNAMED
建议使用场景
-
框架兼容老 API(过渡方案)
-
测试内部类
-
访问 JDK 内部类(谨慎使用,未来可能失效)
--illegal-access
(已废弃)
Java 9 默认允许非法访问,Java 16 起已移除该机制,应避免依赖。
9. 模块化开发最佳实践
随着模块化系统在 Java 9 的引入,构建大型、可维护、可扩展的系统有了更清晰的结构边界和封装机制。本章将从模块划分策略、版本管理、测试方法、演进模式等方面,系统总结模块化开发中的最佳实践,帮助你避免常见陷阱,提升工程质量。
9.1 模块划分与依赖管理建议
1. 每个模块职责单一,围绕“业务边界”划分
良好模块应聚焦单一职责(SRP),例如:
-
user.api
:定义用户操作接口(如服务接口、DTO) -
user.service
:包含用户业务逻辑实现 -
user.persistence
:封装数据库访问
2. 不要过度拆分模块
模块过细会增加管理复杂度,特别是在构建、部署、测试时产生不必要的开销。保持合理粒度。
3. 显式控制模块间依赖
使用 requires
指令定义依赖关系,杜绝隐式引用。通过 exports
精确控制暴露的 API 包,避免无意泄漏内部结构。
4. 防止循环依赖
模块之间不能形成循环依赖。可通过引入“抽象模块”(仅定义接口)中断循环依赖链。
9.2 API 与实现分离策略
推荐将接口与实现分属不同模块:
-
com.example.api
(公共接口,导出) -
com.example.impl
(内部实现,不导出)
这种结构有利于隐藏实现细节、替换实现、进行测试隔离。
9.3 模块版本控制与演进
Java 模块系统不内建版本支持,因此版本管理需由构建工具(如 Maven、Gradle)控制:
-
避免直接依赖 SNAPSHOT 或本地构建 JAR
-
使用语义化版本命名(Semantic Versioning)
-
构建过程建议生成 module descriptor (
module-info.class
) 并记录模块版本元数据
9.4 模块化项目的测试策略
单元测试
-
测试模块可独立运行
-
使用
requires transitive
暴露测试依赖模块 -
使用
opens
指令允许测试框架访问包
集成测试
-
启动主模块,配置模块路径完整覆盖所有依赖
-
利用
--add-opens
临时开放包访问权限
构建工具整合
-
Maven:使用
moditect-maven-plugin
添加 module-info -
Gradle:Java 9+ 插件已支持 module-info 编译
9.5 构建系统中的模块优化技巧
-
使用多模块项目结构简化依赖和打包
-
JAR 文件名规范:
<module-name>-<version>.jar
-
自动生成
module-info.java
初稿后再手动优化
9.6 常见问题与误区
问题 | 原因 | 建议 |
---|---|---|
启动报错“module not found” | 模块路径未正确配置 | 使用 --module-path 指向所有依赖 |
运行时反射失败 | 未开放内部包 | 使用 opens 或 --add-opens |
循环依赖 | 模块设计不合理 | 抽象接口解耦,或合并模块 |
自动模块名冲突 | 多个 JAR 被赋予相同模块名 | 显式定义模块或避免命名重复 |
10. 完整配置参考
一份完整示例的 module-info.java
文件,涵盖所有常见的模块声明语法元素,并在代码中用注释详细说明每个配置的作用和使用场景:
/**
* 示例模块声明文件 module-info.java
* 详细演示了模块系统中的各种指令和配置
*/
module com.example.myapp {
// requires:声明本模块依赖的其他模块
requires java.base; // 所有模块隐式依赖,通常不用写,示例演示
requires java.sql; // 依赖 Java 平台的 SQL 模块
requires transitive com.example.api; // 传递依赖,依赖本模块的模块也会隐式依赖 com.example.api
// requires static:表示编译时需要依赖,但运行时可选
requires static lombok; // 仅编译时需要 lombok 注解处理器,运行时无需
// exports:导出包,允许其他模块访问这些包中的公共类型
exports com.example.myapp.api; // 导出公共 API 包
exports com.example.myapp.utils to com.example.client; // 仅导出给指定模块
// opens:打开包,允许反射访问该包(不等同于 exports)
opens com.example.myapp.internal; // 允许所有模块通过反射访问此包(常用于框架)
opens com.example.myapp.reflection to spring.core; // 仅允许 spring.core 模块反射访问
// uses:声明本模块使用的服务接口
uses com.example.spi.Formatter;
// provides:声明本模块提供的服务实现
provides com.example.spi.Formatter with com.example.myapp.impl.JsonFormatter;
// 注释:module-info.java 文件不能包含类、接口定义,只能包含模块声明相关语句
}