导读:本期聚焦于刘卫东创作的《如何利用 exports to 语法实战实现在特定的“朋友圈”模块内共享底层变量字段》,敬请观看详情。Java 9 引入模块系统后,exports 指令有了一个限定形式 exports to,它允许模块只把包导出给指定的友好模块。这样能精准控制 API 可见范围,避免把内部实现暴露给所有依赖方。实战中很多团队会忽略它和普通 exports 的差异:普通 exports 导出后所有模块都能读取该包中的 public 类型,而限定导出只对 to 后面列出的模块开放。本文围绕朋友圈模块这个比喻场景,展示如何通过 module-info.java 中的 exports to 语法,让 friend 模块访问 core 模块底层的共享变量字段,同时阻止其他无关模块在编译期引用。文章会拆解可见性规则、给出多模块构建示例,并分析反射和 opens 带来的边界问题,帮助读者把 Java 模块化真正用到项目里。

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

如何利用 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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。