当前位置:首页 > 文章列表 > 文章 > java教程 > Java 模块系统怎样导出只读 API 包

Java 模块系统怎样导出只读 API 包

来源:17golang原创 2026-10-26 04:02:20 0浏览 收藏

如果你想让其他模块只能使用一组稳定的 Java API,同时不让它们直接依赖仓储、缓存或实现类,做法不是寻找一个叫“只读导出”的特殊关键字,而是把公开类型放进 API 包,再在 module-info.java 中使用 exports。真正的只读语义还要靠没有 setter 的类型、接口或不可变数据对象来完成。

最小方案是:导出 com.example.catalog.api,不导出 com.example.catalog.internal;消费者模块声明 requires com.example.catalog,只能依赖 API 包中的 public 类型。若只允许某个模块使用,再改成限定导出 exports ... to ...。

官方参考地址:https://docs.oracle.com/javase/specs/jls/se25/html/jls-7.html

先区分导出边界与只读语义

这次问题通常表现为两种症状:消费者模块要么无法 import 提供者模块里的类,要么虽然能 import,却能通过 setter 或可变集合改动提供者返回的对象。前一种是模块导出边界没有配置好,后一种不是 exports 失效,而是 API 类型本身暴露了写入口。

Java 模块系统把“能不能从模块外访问”拆成了两层:

  • 类型必须是 public,否则其他包不能正常使用它;
  • 类型所在的包必须被模块导出,否则即使类型是 public,也只对模块内部代码可见。

所以,下面的 API 可以被称为“只读 API 表面”:消费者得到的是 ProductView,它没有 setter;实现细节藏在未导出的内部包中。这里的只读是设计结果,不是模块系统替你冻结了所有对象。

Java 模块中消费者模块、API 包、ProductView 和内部实现包的静态边界关系
图1:Java 模块 API 边界说明图,展示导出的公共包与内部实现包之间的静态关系。这是原创结构图,不是运行截图。

把 API 包和实现包放在不同边界

先按职责拆包。com.example.catalog.api 只放消费者需要知道的类和不可变结果;com.example.catalog.internal 放仓储、缓存和查询实现。包名是精确边界,导出前者不会顺带导出名字更深的子包。

// API 包:只暴露查询入口和不可变结果对象
package com.example.catalog.api;

public record ProductView(String id, String name) {
    // record 组件只有访问器,没有 setter,适合作为只读结果视图
}

public final class Catalog {
    private final com.example.catalog.internal.CatalogRepository repository;

    public Catalog() {
        // 实现类仍可在同一模块内部被使用
        this.repository = new com.example.catalog.internal.CatalogRepository();
    }

    public ProductView find(String id) {
        // 对外只返回 API 包里的结果类型,不泄露仓储对象
        return repository.find(id);
    }
}
// 内部包:类可以是 public,但整个包不导出时模块外仍不可访问
package com.example.catalog.internal;

import com.example.catalog.api.ProductView;

public final class CatalogRepository {
    public ProductView find(String id) {
        // 示例只表达对象边界;真实项目中这里可以接数据库或缓存
        return new ProductView(id, "示例商品");
    }
}

这里有一个容易忽略的点:CatalogRepository 即使声明为 public,只要它所在的 internal 包没有导出,外部模块仍不能把它当作普通 API 使用。这样做可以让仓储实现随时替换,而不要求调用方跟着改依赖。

在 module-info.java 导出 API 包

提供者模块的描述文件只需要导出 API 包。requires java.base 不必手写,因为它会被隐式读取;如果项目还依赖其他命名模块,再补充对应的 requires。

// 提供者模块:只把稳定 API 包放到模块的对外表面
module com.example.catalog {
    exports com.example.catalog.api;
}

消费者模块必须明确读取提供者模块:

// 消费者模块:声明它要读取 catalog 模块
module com.example.app {
    requires com.example.catalog;
}

消费者代码只依赖导出的类型:

package com.example.app;

import com.example.catalog.api.Catalog;
import com.example.catalog.api.ProductView;

public final class Main {
    public static void main(String[] args) {
        // 调用方拿到只读视图,而不是内部仓储
        ProductView view = new Catalog().find("p-100");
        System.out.println(view.name());
    }
}

在模块化源码目录中,常见的编译入口可以写成下面这样。命令中的注释只解释参数用途,不代表本文在本机执行了这组代码:

# 将多个模块编译到按模块分组的输出目录
javac -d out --module-source-path src -m com.example.catalog,com.example.app

# 从模块路径启动消费者模块的 Main 类
java --module-path out -m com.example.app/com.example.app.Main

如果消费者尝试导入 com.example.catalog.internal.CatalogRepository,问题不在 classpath,而在模块声明:该包没有出现在 exports 中。把整个模块改成 open module 也不会让它变成正常编译期 API。

需要收窄时使用限定导出

无条件导出适合公共库;如果 API 只给一个应用模块或一组明确的协作模块使用,可以加 to。这叫限定导出,模块外只有列出的命名模块可以访问这个包。

// 只允许指定应用模块读取 API 包
module com.example.catalog {
    exports com.example.catalog.api to com.example.app;
}

例如,com.example.app 可以读取 com.example.catalog.api,而没有列在 to 后面的 com.example.test 即使声明了 requires com.example.catalog,也不能把这个包当作普通可见包使用。内部包仍然不导出。

Java 限定 exports 只允许指定模块读取 API 包的静态关系
图2:限定导出关系图,展示指定模块可以读取 API 包而内部包仍不对外开放。这是原创结构图,不是运行结果截图。

可以用下面的表快速判断该选哪种声明:

声明外部可见范围适合场景
exports api;所有读取该模块的命名模块公共库的稳定 API
exports api to app;仅指定命名模块应用与库之间的受控协作
opens api;主要面向反射访问框架需要反射读取成员时
不写 exports模块内部代码实现包、测试辅助或内部适配器

按可见性和反射边界排查

遇到“导出了却不能用”时,按下面的顺序判断,通常比直接添加 --add-exports 更容易找到根因:

  1. 确认导出的包名和 import 的包名完全一致。com.example.catalog.api 不包含 com.example.catalog.api.internal。
  2. 确认被引用的顶层类或接口是 public。导出包不会把包级可见类自动变成 public。
  3. 确认消费者模块写了 requires com.example.catalog,并且编译、运行时使用的是模块路径。
  4. 如果只是框架反射失败,再检查是否应该使用 opens;不要为了普通 import 把内部包开放给反射。
  5. 如果要求“只读”,检查返回值是否暴露可变集合、setter 或能修改提供者状态的对象引用。

最后一点很关键:exports 解决“谁能看到这个包”,不解决“看到之后能否改变对象”。例如,对外返回 List 时,调用方仍可能尝试修改列表;如果 API 需要只读集合,应在返回边界提供不可变视图或不可变副本,并在文档中写清所有权。

结论:把“只读 API”拆成两项配置

Java 模块系统导出只读 API 包的稳定做法可以压缩成两项:第一,在包结构上只导出 api,不导出 internal;第二,在类型设计上让 API 只提供查询、不可变结果和受控接口。默认公共库用无条件 exports,内部协作用限定 exports ... to ...,反射框架才考虑 opens。

这样一来,消费者依赖的是一份清晰的契约,而不是提供者当前的实现目录。以后更换仓储、缓存或内部类时,只要 API 包的契约不变,模块边界就能继续替你挡住不必要的耦合。

相关问题

导出一个包会自动导出它的子包吗?

不会。Java 模块声明按完整包名匹配,父包与子包需要分别决定是否导出。

只写 public 不写 exports 可以吗?

对模块外的命名模块不够。顶层类型除了要是 public,所在包也必须被导出。

exports 和 opens 有什么区别?

exports 面向普通编译与运行期 API 访问;opens 主要面向反射,并不替代消费者对公开类型的正常模块依赖。

限定导出能阻止 API 对象被修改吗?

不能。限定导出只限制哪些模块能访问包,是否可修改要由 public 方法、字段、集合和对象模型共同决定。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Python asyncio.TaskGroup 怎样在任务组结束时集中处理异常Python asyncio.TaskGroup 怎样在任务组结束时集中处理异常
上一篇
Python asyncio.TaskGroup 怎样在任务组结束时集中处理异常
Python os.scandir 怎样减少目录遍历中的属性查询
下一篇
Python os.scandir 怎样减少目录遍历中的属性查询
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    695次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    707次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    776次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    736次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    569次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码