You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让java.net.http.HttpClient报告大文件下载进度?

实现Java HttpClient大文件下载进度跟踪

要解决HttpClient下载大文件时无法反馈进度的问题,我们可以通过两种核心方式实现进度监控:自定义响应体处理器/订阅者(符合HttpClient响应式设计),或者手动读取输入流并统计字节(直观易上手)。以下是具体实现方案:


方法一:自定义BodyHandler与BodySubscriber(推荐,支持同步/异步)

通过包装默认的文件写入订阅者,在字节写入过程中统计下载量并触发进度回调,既保留HttpClient的原生特性,又能灵活实现进度通知。

1. 定义进度监听接口

public interface DownloadProgressListener {
    // 进度更新回调:已下载字节数、总字节数(-1表示未知)
    void onProgress(long downloadedBytes, long totalBytes);
    // 可选:错误回调
    default void onError(Throwable error) {
        System.err.println("下载出错: " + error.getMessage());
    }
}

2. 实现进度跟踪的BodyHandler

import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.net.http.HttpResponse;

public class ProgressTrackingFileHandler implements HttpResponse.BodyHandler<Path> {
    private final Path destination;
    private final DownloadProgressListener listener;
    private long totalBytes;

    public ProgressTrackingFileHandler(Path destination, DownloadProgressListener listener) {
        this.destination = destination;
        this.listener = listener;
    }

    @Override
    public HttpResponse.BodySubscriber<Path> apply(HttpResponse.ResponseInfo responseInfo) {
        // 从响应头获取文件总大小(部分场景可能不存在)
        totalBytes = responseInfo.headers().firstValueAsLong("Content-Length").orElse(-1);
        // 创建默认的文件写入订阅者
        HttpResponse.BodySubscriber<Path> fileSubscriber = HttpResponse.BodySubscribers.ofFile(
                destination, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
        // 包装为带进度跟踪的订阅者
        return new ProgressTrackingSubscriber(fileSubscriber, totalBytes, listener);
    }
}

3. 实现进度跟踪的BodySubscriber

import java.nio.ByteBuffer;
import java.util.List;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Flow;

public class ProgressTrackingSubscriber implements HttpResponse.BodySubscriber<Path> {
    private final HttpResponse.BodySubscriber<Path> delegate;
    private final long totalBytes;
    private final DownloadProgressListener listener;
    private long downloadedBytes = 0;
    // 进度更新间隔:每下载1MB触发一次回调
    private static final long PROGRESS_INTERVAL = 1024 * 1024;
    private long lastReportedBytes = 0;

    public ProgressTrackingSubscriber(HttpResponse.BodySubscriber<Path> delegate, long totalBytes, DownloadProgressListener listener) {
        this.delegate = delegate;
        this.totalBytes = totalBytes;
        this.listener = listener;
    }

    @Override
    public CompletionStage<Path> getBody() {
        return delegate.getBody();
    }

    @Override
    public void onSubscribe(Flow.Subscription subscription) {
        delegate.onSubscribe(subscription);
    }

    @Override
    public void onNext(List<ByteBuffer> items) {
        // 统计本次读取的字节数
        long batchBytes = items.stream().mapToLong(ByteBuffer::remaining).sum();
        downloadedBytes += batchBytes;

        // 达到间隔或下载完成时触发进度更新
        if (downloadedBytes - lastReportedBytes >= PROGRESS_INTERVAL || downloadedBytes == totalBytes) {
            listener.onProgress(downloadedBytes, totalBytes);
            lastReportedBytes = downloadedBytes;
        }

        delegate.onNext(items);
    }

    @Override
    public void onError(Throwable throwable) {
        delegate.onError(throwable);
        listener.onError(throwable);
    }

    @Override
    public void onComplete() {
        delegate.onComplete();
        listener.onProgress(downloadedBytes, totalBytes); // 最后一次更新进度
    }
}

4. 使用示例(同步下载)

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
import java.time.Duration;

public class DownloadWithProgress {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newBuilder()
                .followRedirects(HttpClient.Redirect.NORMAL)
                .connectTimeout(Duration.ofSeconds(20))
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://example.com/large-file"))
                .build();

        Path destination = Path.of("./downloaded-large-file");

        // 终端打印进度的监听实现
        DownloadProgressListener consoleListener = (downloaded, total) -> {
            if (total != -1) {
                double progressPercent = (double) downloaded / total * 100;
                System.out.printf("\r已下载: %,d/%,d 字节 (%.1f%%)", downloaded, total, progressPercent);
            } else {
                System.out.printf("\r已下载: %,d 字节 (未知总大小)", downloaded);
            }
        };

        // 执行下载
        HttpResponse<Path> response = client.send(request, new ProgressTrackingFileHandler(destination, consoleListener));
        System.out.println("\n下载完成!文件路径:" + response.body());
    }
}

异步下载示例

如果需要避免阻塞主线程(如GUI应用),可使用sendAsync方法:

client.sendAsync(request, new ProgressTrackingFileHandler(destination, consoleListener))
        .thenApply(HttpResponse::body)
        .thenAccept(path -> System.out.println("\n异步下载完成!文件路径:" + path))
        .exceptionally(error -> {
            consoleListener.onError(error);
            return null;
        });

// 主线程可继续处理其他逻辑,控制台程序可添加阻塞等待
// Thread.currentThread().join();

方法二:手动读取InputStream(简单直观)

直接获取响应的输入流,手动读写并统计字节,适合快速实现同步下载进度跟踪:

import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
import java.time.Duration;

public class SimpleProgressDownload {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newBuilder()
                .followRedirects(HttpClient.Redirect.NORMAL)
                .connectTimeout(Duration.ofSeconds(20))
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://example.com/large-file"))
                .build();

        HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
        long totalBytes = response.headers().firstValueAsLong("Content-Length").orElse(-1);
        InputStream inputStream = response.body();
        Path destination = Path.of("./downloaded-file");
        byte[] buffer = new byte[8192]; // 8KB缓冲区,可根据需求调整
        int bytesRead;
        long downloadedBytes = 0;
        long lastReportedBytes = 0;
        final long UPDATE_INTERVAL = 1024 * 1024; // 每1MB更新一次

        try (FileOutputStream outputStream = new FileOutputStream(destination.toFile())) {
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                downloadedBytes += bytesRead;

                if (downloadedBytes - lastReportedBytes >= UPDATE_INTERVAL || (totalBytes != -1 && downloadedBytes == totalBytes)) {
                    if (totalBytes != -1) {
                        double progress = (double) downloadedBytes / totalBytes * 100;
                        System.out.printf("\r已下载: %,d/%,d 字节 (%.1f%%)", downloadedBytes, totalBytes, progress);
                    } else {
                        System.out.printf("\r已下载: %,d 字节", downloadedBytes);
                    }
                    lastReportedBytes = downloadedBytes;
                }
            }
        }
        System.out.println("\n下载完成!");
    }
}

注意事项

  1. Content-Length缺失处理:部分服务器采用分块编码传输,无法获取文件总大小,此时需兼容totalBytes = -1的情况。
  2. GUI场景适配:如果是Swing/JavaFX应用,进度更新需切换到UI线程执行(如SwingUtilities.invokeLater或Platform.runLater),避免线程安全问题。
  3. 进度间隔调整:可根据需求修改PROGRESS_INTERVAL或UPDATE_INTERVAL,平衡性能与通知频率。

内容的提问来源于stack exchange,提问作者Lii

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 23:50:04