Java 9 引入平台模块系统之后,module-info.java 中的 exports 指令成为控制包可见性的核心手段。通常我们写的是不加限定的 exports,比如 exports com.example.internal;,意思是把这个包导出给所有依赖当前模块的其他模块。但项目规模变大后,这样的导出会让内部 API 扩散到无关模块。exports to 语法正是为解决这个问题而生的,它允许你把一个包只导出给指定列表中的若干模块,形成一个受信任的“朋友圈”。

一、exports to 的语法与可见性模型
在 Java 模块描述文件 module-info.java 中,exports 指令有两种形式。第一种是无条件导出,它把包公开给所有读取当前模块的模块。第二种是限定导出,语法为 exports 包名 to 模块名, 模块名;。例如需要把 com.example.internal 包只开放给 com.example.friend 模块时,可以这样声明:
module com.example.core {
exports com.example.internal to com.example.friend;
}
这种写法带来的效果非常明确:com.example.friend 模块可以在编译期和运行时读取 com.example.internal 包里的 public 类型和成员,而其他模块即使在自己的 module-info.java 中写上了 requires com.example.core;,也会因为该包没有被导出给自己而无法访问。需要特别注意的是,限定导出只对包内的 public 类型生效,包私有的类、字段和方法仍然不跨包可见。因此,想让朋友圈模块共享底层变量字段,字段本身必须是 public,或者通过 public 方法暴露。
对比无条件导出,限定导出并没有削弱模块边界,而是把边界从“所有模块”收窄到了“指定模块”。这非常适合微服务或者多模块工程中的内部协作场景:core 模块负责维护底层状态,friend 模块是经过设计的内部消费者,而 other 模块则被挡在编译期之外。一个常见的误用是以为 exports to 能限制反射访问,实际上它管不到深反射,限制反射需要依赖 opens 指令,这一点会在后文解释。
二、在朋友圈模块内共享底层变量字段的设计
假设项目中有三个模块:com.example.core 负责底层数据管理,com.example.friend 是朋友圈模块,com.example.other 是外部调用方。现在 core 模块内部有一个共享状态类,里面保存了活跃用户数、最近更新人等变量,这些变量只需要 friend 模块读写,不能被 other 模块直接触碰。
首先在 core 模块中放置共享变量。为了让 friend 模块能访问,字段必须声明为 public,或者提供 public 的 getter 和 setter。下面是一个最小示例:
package com.example.internal;
public class SharedState {
public static int activeUserCount = 0;
public String lastUpdateBy;
public void incrementUserCount() {
activeUserCount++;
}
}
虽然 activeUserCount 是一个静态共享变量,但它在包 com.example.internal 中。如果 core 模块没有导出这个包,friend 模块即使 import 也会编译失败。所以 core 模块的 module-info.java 需要声明限定导出:
module com.example.core {
exports com.example.internal to com.example.friend;
}
接下来 friend 模块要声明对 core 模块的依赖,并编写访问代码。module-info.java 如下:
module com.example.friend {
requires com.example.core;
}
friend 模块的类可以正常导入并修改共享字段:
package com.example.friend;
import com.example.internal.SharedState;
public class FriendWorker {
public void updateSharedState() {
SharedState.activeUserCount++;
SharedState state = new SharedState();
state.lastUpdateBy = "friend";
System.out.println(SharedState.activeUserCount);
System.out.println(state.lastUpdateBy);
}
}
这段代码编译和运行都没有问题,因为 core 已经把 com.example.internal 导出给了 friend。如果 other 模块尝试写同样的 import 语句,编译器会直接报告 com.example.internal 包不可见,错误信息类似“package com.example.internal is not visible”。这样就从构建层面阻止了未经授权的底层字段访问,而不是靠开发者的口头约定。
三、多模块实战:编译与验证访问控制
要完整验证 exports to 的效果,可以手工创建三个模块目录,用 javac 命令编译。目录结构大致如下:
project/
├── core/
│ ├── module-info.java
│ └── com/example/internal/SharedState.java
├── friend/
│ ├── module-info.java
│ └── com/example/friend/FriendWorker.java
└── other/
├── module-info.java
└── com/example/other/OtherWorker.java
先编译 core 模块,输出到 out/core:
javac -d out/core $(find core -name "*.java")
然后编译 friend 模块,并把 core 的编译结果放到模块路径中:
javac -d out/friend --module-path out/core $(find friend -name "*.java")
这条命令会成功。接下来尝试编译 other 模块,other 的 module-info.java 同样声明 requires com.example.core;,但 core 并没有把内部包导出给它。执行:
javac -d out/other --module-path out/core:out/friend $(find other -name "*.java")
编译会失败,提示 other 模块中的类无法访问 com.example.internal 包。此时可以发现,exports to 的限定在编译期就生效了,比运行期检查更早发现问题。如果把 core 的 module-info.java 改成无条件导出:
module com.example.core {
exports com.example.internal;
}
再重新编译 core,other 模块的编译就会通过。这个对比实验能直观理解限定导出的边界控制能力。实际项目中通常会使用 Maven 或 Gradle 管理多模块,工具会读取 module-info.java 并自动处理模块路径,但底层规则与手工 javac 完全一致。
四、常见误区:exports to、opens 与反射访问
很多开发者会把 exports 和 opens 混为一谈。exports 控制编译期和运行时的可读性,也就是你能不能 import 一个包中的类型;opens 控制的是深层反射,也就是能不能通过 setAccessible(true) 访问私有成员。如果只想让 friend 模块通过反射读写底层字段,只有 exports to 是不够的,还需要 opens to 指令。
可以这样同时声明:
module com.example.core {
exports com.example.internal to com.example.friend;
opens com.example.internal to com.example.friend;
}
这样 friend 模块既能正常引用 public 类型,也能在需要时反射访问私有字段。如果只声明 exports to 而没有 opens,运行阶段调用 Field.setAccessible(true) 会抛 InaccessibleObjectException。反过来,如果只需要反射而不需要编译期引用,只写 opens to 就够了。
另一个容易忽略的点是依赖的传递性。exports to 只是限制了某一层导出,不会自动把依赖模块再暴露给下一个模块。比如 friend 模块还依赖了一个 helper 模块,other 也不能因为依赖 friend 就间接访问 core 的内部包。模块系统要求每一层访问都必须有明确的导出和读取关系。对于内部共享场景,建议在 core 模块里用 requires transitive 谨慎传递公开 API,但不要把它和 exports to 混用,否则会让内部包意外扩散。
最后需要强调,exports to 的“朋友圈”列表可以包含多个模块,用逗号分隔即可。例如 exports com.example.internal to com.example.friend, com.example.audit;。当后续模块数量增加时,只需要修改 core 的 module-info.java 并重新编译,所有未经列出的模块依然无法访问,这为底层变量字段的共享提供了一种静态、可审计的约束方式。
通过以上实战可以看到,exports to 适合在模块化工程中建立受控内部共享通道。它不会引入运行时代理,也不依赖反射,而是在编译期强制实施访问边界。设计时需要区分 public 字段与包私有字段、exports 与 opens 的边界,才能真正用好 Java 模块系统。
Java模块化exports toJPMS修改时间:2026-08-24 22:32:07