如何让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下载完成!"); } }
注意事项
- Content-Length缺失处理:部分服务器采用分块编码传输,无法获取文件总大小,此时需兼容
totalBytes = -1的情况。 - GUI场景适配:如果是Swing/JavaFX应用,进度更新需切换到UI线程执行(如
SwingUtilities.invokeLater或Platform.runLater),避免线程安全问题。 - 进度间隔调整:可根据需求修改
PROGRESS_INTERVAL或UPDATE_INTERVAL,平衡性能与通知频率。
内容的提问来源于stack exchange,提问作者Lii
相关产品推荐
相关产品推荐

