在云原生时代,将大文件分片上传到对象存储(如 Aliyun OSS、AWS S3、自建对象存储等)已经成为非常常见的场景。但在 Java 中,如果使用标准 HTTP 客户端(HttpClient)实现 分片上传(Multipart Upload),往往会遇到一个问题:
如何高效地从文件中读取指定范围的数据并发送?
在 OpenJDK 26 中,这个问题得到了官方改进。Java 的 HTTP 客户端新增了一个实用 API:
HttpRequest.BodyPublishers.ofFileChannel(FileChannel chan, long position, long size)
这个 API 的加入,让 大文件分片并行上传 变得更加简单且高效。
为什么需要分片上传?
传统分片上传的逻辑通常分为三步:
- 将一个大文件拆分成多个部分(Part)
- 每个部分通过 独立 HTTP 请求并行上传
- 上传完成后由服务端重新组合成完整文件
例如,一个 10GB 文件 可能被拆成 100MB × 100 个分片,然后并行上传。这能显著提升上传速度,并具备更好的容错性。
JDK 26 之前的困境
在 JDK 26 之前的版本中,HttpClient 只提供了 上传整个文件 的 API:
HttpRequest.BodyPublishers.ofFile(Path)
如果你的需求是上传文件的某个片段(例如第2个100MB),就需要自己动手:
- 手动读取文件的指定范围
- 将这部分数据拷贝到内存(如
ByteBuffer 或字节数组)
- 再创建对应的
BodyPublisher(例如 ofByteArray)
这种做法带来的问题显而易见:
- 额外内存消耗:每个分片都需要在内存中保留一份副本。
- 数据复制开销:从文件到内存,再从内存到网络,多了一次不必要的拷贝。
- 代码复杂度增加:开发者需要处理文件读取、范围控制、资源关闭等一系列细节。
在高并发、大文件的分片上传场景下,这种模式既不高效,也不优雅。
JDK 26 的解决方案:ofFileChannel
JDK 26 引入的新 API 直击痛点:
HttpRequest.BodyPublishers.ofFileChannel(
FileChannel channel,
long position,
long size
)
它的作用很明确:从指定的 FileChannel 的特定位置开始,读取并发送指定大小的数据。
参数说明如下:
| 参数 |
含义 |
FileChannel chan |
已打开的文件通道 |
position |
文件读取的起始位置(字节偏移量) |
size |
要发送的字节数 |
这意味着:
- 可以 直接从文件读取指定区间,实现了真正的“零拷贝”式读取。
- 不需要将整个分片数据加载到堆内存,极大减少了内存压力。
- 代码简洁,意图清晰,非常适合分片上传 场景。
实战代码示例
假设我们有一个文件,需要并行上传多个分片,现在可以这样实现:
// 打开一个文件通道,供多个上传请求共享
FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
// 构建上传第一个分片(例如 0-100MB)的请求
HttpRequest requestPart1 = HttpRequest.newBuilder(uploadUri)
.POST(HttpRequest.BodyPublishers.ofFileChannel(channel, 0L, 100 * 1024 * 1024L))
.build();
// 构建上传第二个分片(例如 100MB-200MB)的请求
HttpRequest requestPart2 = HttpRequest.newBuilder(uploadUri)
.POST(HttpRequest.BodyPublishers.ofFileChannel(channel, 100 * 1024 * 1024L, 100 * 1024 * 1024L))
.build();
// 可以使用 HttpClient 并发发送 requestPart1 和 requestPart2
// ... channel 在所有请求完成后关闭
每个上传请求通过指定不同的 offset 和 length,就能轻松实现:
- 并行读取同一个文件:多个 HTTP 请求可以并发地从同一个
FileChannel 读取数据。
- 各自上传不同区间:互不干扰,逻辑清晰。
整个过程无需任何额外的数据复制操作,效率和资源利用率都得到了优化。对于希望深入探讨这类Java性能优化技术的开发者,这是一个值得关注的进步。
为什么是 ofFileChannel 而不是 ofFile?
你可能会有疑问:为什么新 API 不设计成 ofFile(Path, long position, long size),让内部去处理文件打开和关闭呢?OpenJDK 开发者们确实考虑过这个方案,但最终选择了 ofFileChannel,主要基于以下两点原因:
1. 更好的资源控制
如果 API 内部自动打开文件,每次调用都可能隐式创建一个新的 FileChannel。在高并发上传数百个分片时,这可能意味着数百个文件描述符被同时打开又关闭,其生命周期难以由应用层精细控制,可能引发资源竞争或耗尽问题。
而 ofFileChannel 将 FileChannel 的控制权交给了应用层。开发者可以共享一个 FileChannel,在所有上传任务完成后统一关闭,实现资源利用的最大化。
2. 更契合高并发上传场景
分片上传的核心诉求就是高并发。使用单个可共享的 FileChannel,完美契合了“多线程/多任务读取同一文件不同部分”的需求,避免了重复打开文件的系统开销,使得整个上传过程更加高效和可控。
结语
HttpRequest.BodyPublishers.ofFileChannel 的加入,虽然只是 JDK 标准库中一个看似微小的 API 补充,但它精准地解决了 Java 开发者在大文件处理,特别是分片上传场景中的一个实际痛点。它遵循了“将资源控制权交给开发者”的设计哲学,通过提供更底层的、基于 FileChannel 的接口,实现了高效、低耗的文件区间读取与网络发送。对于正在构建云存储、数据同步或媒体处理服务的团队来说,这个特性值得在未来的 JDK 26 升级规划中重点关注。