Java 9 模块化系统(Project Jigsaw)深度解析_java9 模块化
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):模块可声明使用/提供服务,便于松耦合编程。
模块与包、类的区别:
模块是对包和类组织的进一步抽象,用于定义更大粒度的访问边界和依赖管理。
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 类
为帮助大家更系统性理解模块的抽象层次,我们可以从代码组织结构上进行比较:
private
、protected
exports
/opens
module-info.java
)requires
示例对比说明:
假设我们开发了一个图书管理系统,其中包含三个模块:
-
com.library.core
:核心功能模块 -
com.library.db
:数据访问模块,依赖core
-
com.library.app
:应用启动模块,依赖core
和db
每个模块都包含若干包,如:
com.library.core.apicom.library.core.implcom.library.db.daocom.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 模块路径与类路径的区别
模块路径和类路径虽然都用于定位类文件,但它们在模块系统中的作用完全不同:
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-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-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 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.jarjava --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 文件名规范:
-.jar
-
自动生成
module-info.java
初稿后再手动优化
9.6 常见问题与误区
--module-path
指向所有依赖opens
或 --add-opens
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 文件不能包含类、接口定义,只能包含模块声明相关语句}