1. 概述
本文将介绍在 Java 中下载文件的多种方法,涵盖从基础的 Java IO 到 NIO,再到常用第三方库如 AsyncHttpClient 和 Apache Commons IO 的实现。
同时,我们还会探讨如何实现断点续传功能,避免在网络中断后重新下载整个文件。
这些方法各有适用场景,掌握它们有助于你在不同需求下做出合理选择,避免踩坑。
2. 使用 Java IO
最基础的方式是使用 Java 的 java.io
包。核心思路是通过 URL
类打开一个网络连接,获取输入流,然后写入本地文件。
try (BufferedInputStream in = new BufferedInputStream(new URL(FILE_URL).openStream());
FileOutputStream fileOutputStream = new FileOutputStream(FILE_NAME)) {
byte dataBuffer[] = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
fileOutputStream.write(dataBuffer, 0, bytesRead);
}
} catch (IOException e) {
// 处理异常
}
✅ 关键点:
- 使用
BufferedInputStream
可显著提升性能,减少系统调用次数。 - 每次读取固定大小的 buffer(如 1024 字节),避免单字节读取带来的频繁上下文切换。
⚠️ 但注意: 上面代码虽然用了 BufferedInputStream
,但由于我们已经用 byte[1024]
批量读取,其缓冲作用并不明显。真正性能瓶颈在于数据会经过 JVM 堆内存。
更简洁的写法(Java 7+)
Java 7 引入了 java.nio.file.Files
工具类,可以用一行代码完成复制:
InputStream in = new URL(FILE_URL).openStream();
Files.copy(in, Paths.get(FILE_NAME), StandardCopyOption.REPLACE_EXISTING);
✅ 优点: 代码简洁,语义清晰。
❌ 缺点: 依然是将数据先读入内存缓冲区,不适合超大文件。
3. 使用 NIO(零拷贝优化)
如果你追求极致性能,Java NIO 提供了 通道(Channel)间直接传输的能力,避免数据拷贝到 JVM 内存,即所谓的“零拷贝(zero-copy)”。
实现步骤:
- 将 URL 流包装为
ReadableByteChannel
- 获取目标文件的
FileChannel
- 使用
transferFrom
直接传输
try (ReadableByteChannel readableByteChannel = Channels.newChannel(new URL(FILE_URL).openStream());
FileOutputStream fileOutputStream = new FileOutputStream(FILE_NAME);
FileChannel fileChannel = fileOutputStream.getChannel()) {
fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE);
} catch (IOException e) {
// 处理异常
}
✅ 优势:
- 数据可直接从内核缓冲区写入磁盘,不经过 JVM 堆内存
- 在 Linux/Unix 系统上利用
sendfile
系统调用,减少用户态与内核态的上下文切换 - 性能更高,尤其适合大文件传输
⚠️ 注意: 并非所有操作系统都支持真正的零拷贝,实际效果依赖底层 OS 支持。
4. 使用第三方库
对于日常开发,使用成熟库更省心。虽然性能不一定最优,但 API 更友好,功能更全。
4.1 AsyncHttpClient(异步下载)
适用于需要非阻塞、异步下载的场景。基于 Netty 实现,支持高效异步 HTTP 请求。
添加依赖(Maven):
<dependency>
<groupId>org.asynchttpclient</groupId>
<artifactId>async-http-client</artifactId>
<version>2.12.3</version>
</dependency>
示例代码:
AsyncHttpClient client = Dsl.asyncHttpClient();
FileOutputStream stream = new FileOutputStream(FILE_NAME);
client.prepareGet(FILE_URL).execute(new AsyncCompletionHandler<FileOutputStream>() {
@Override
public State onBodyPartReceived(HttpResponseBodyPart bodyPart) throws Exception {
stream.getChannel().write(bodyPart.getBodyByteBuffer());
return State.CONTINUE;
}
@Override
public FileOutputStream onCompleted(Response response) throws Exception {
stream.close();
return stream;
}
@Override
public void onThrowable(Throwable t) {
// 处理异常
}
});
✅ 关键技巧:
- 重写
onBodyPartReceived
,避免默认将所有 chunk 缓存到内存(否则大文件会 OOM) - 使用
FileChannel.write(ByteBuffer)
直接写入文件,ByteBuffer 内存位于堆外(off-heap),不占用 JVM 堆空间
⚠️ 踩坑提醒: 不要直接用 bodyPart.getBodyAsBytes()
,这会把整个 chunk 加载进堆内存!
4.2 Apache Commons IO
Apache Commons IO 是 Java 开发中几乎标配的工具库,提供了非常简洁的文件操作 API。
添加依赖:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
一行代码下载:
FileUtils.copyURLToFile(
new URL(FILE_URL),
new File(FILE_NAME),
10000, // connect timeout
10000 // read timeout
);
✅ 优点:
- 极简 API,适合脚本化或快速开发
- 支持设置连接和读取超时,避免无限等待
- 底层仍使用
InputStream
循环读取,安全可靠
❌ 缺点: 性能与基础 IO 相当,无零拷贝优化
5. 实现断点续传
网络不稳定时,从头开始下载大文件非常浪费。我们可以通过 HTTP 的 Range
头实现断点续传。
实现原理:
- 先用
HEAD
请求获取远程文件总大小 - 检查本地文件已下载大小
- 使用
Range: bytes=x-
请求剩余部分 - 以追加模式打开文件输出流
URL url = new URL(FILE_URL);
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setRequestMethod("HEAD");
long remoteFileSize = httpConnection.getContentLengthLong();
File outputFile = new File(FILE_NAME);
long existingFileSize = outputFile.length();
if (existingFileSize < remoteFileSize) {
HttpURLConnection resumeConnection = (HttpURLConnection) url.openConnection();
resumeConnection.setRequestProperty("Range", "bytes=" + existingFileSize + "-");
try (InputStream in = resumeConnection.getInputStream();
FileOutputStream fos = new FileOutputStream(outputFile, true)) { // ✅ 追加模式
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
}
✅ 关键点:
HEAD
请求不下载内容,仅获取元信息(如Content-Length
)Range: bytes=1024-
表示从第 1024 字节开始下载到末尾new FileOutputStream(file, true)
以追加模式打开文件
⚠️ 注意: 服务器必须支持 Range
请求(返回状态码 206 Partial Content),否则会忽略该头并返回完整文件(200)。
6. 总结
方法 | 适用场景 | 是否推荐 |
---|---|---|
✅ Java IO + buffer | 小文件、简单脚本 | ⚠️ 基础可用,但不高效 |
✅ Java NIO transferFrom |
大文件、高性能要求 | ✅ 强烈推荐 |
✅ AsyncHttpClient | 异步、非阻塞场景 | ✅ 推荐用于高并发 |
✅ Apache Commons IO | 快速开发、工具类 | ✅ 推荐用于简单任务 |
✅ 断点续传 | 网络不稳定、大文件 | ✅ 必备技能 |
📌 最终建议:
- 日常开发用
FileUtils.copyURLToFile
最省事 - 高性能场景优先考虑 NIO 的
transferFrom
- 异步需求选 AsyncHttpClient,记得手动处理 body 写入避免 OOM
- 大文件务必支持断点续传,提升用户体验
示例代码已整理至 GitHub:https://github.com/java-tutorials/file-download-examples