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)”。

实现步骤:

  1. 将 URL 流包装为 ReadableByteChannel
  2. 获取目标文件的 FileChannel
  3. 使用 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 头实现断点续传

实现原理:

  1. 先用 HEAD 请求获取远程文件总大小
  2. 检查本地文件已下载大小
  3. 使用 Range: bytes=x- 请求剩余部分
  4. 以追加模式打开文件输出流
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


原始标题:Download a File From an URL in Java