杰瑞科技汇

HttpClient Java 下载如何实现?

Java 11+

从 Java 11 开始,java.net.http.HttpClient 成为官方标准库的一部分,无需添加任何外部依赖,这是目前推荐的用法。

HttpClient Java 下载如何实现?-图1
(图片来源网络,侵删)

如果您仍在使用 Java 8 或更早版本,您可以使用 Apache HttpClient 或 OkHttp,但本文将重点介绍现代的 Java HttpClient


示例 1:最简单的下载(同步方式)

这个示例展示了最基本的下载功能:将一个文件从 URL 下载到本地文件系统。

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class SimpleDownload {
    public static void main(String[] args) {
        // 1. 定义下载的 URL 和本地保存路径
        String fileUrl = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf";
        Path destinationPath = Paths.get("dummy.pdf");
        // 2. 创建 HttpClient
        HttpClient httpClient = HttpClient.newHttpClient();
        // 3. 创建 HttpRequest
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(fileUrl))
                .build();
        // 4. 发送请求并处理响应
        try {
            HttpResponse<Path> response = httpClient.send(
                    request,
                    HttpResponse.BodyHandlers.ofFile(destinationPath)
            );
            // 5. 检查响应状态码
            if (response.statusCode() == 200) {
                System.out.println("文件下载成功!");
                System.out.println("文件已保存至: " + destinationPath.toAbsolutePath());
            } else {
                System.err.println("下载失败,HTTP 状态码: " + response.statusCode());
            }
        } catch (IOException | InterruptedException e) {
            System.err.println("下载过程中发生错误: " + e.getMessage());
            // 如果是中断异常,需要恢复中断状态
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

代码解析:

  1. HttpClient.newHttpClient(): 创建一个默认配置的 HttpClient 实例。
  2. HttpRequest.newBuilder().uri(...).build(): 构建一个 GET 请求(GET 是默认方法)。
  3. HttpResponse.BodyHandlers.ofFile(destinationPath): 这是关键部分,它告诉 HttpClient 将响应体(即文件内容)直接写入到指定的 Path 对象代表的文件中,这种方式非常高效,因为它利用了 NIO 的零拷贝特性,内存占用极低。
  4. httpClient.send(...): 发送请求并阻塞,直到收到响应。
  5. try-catch: 处理可能发生的 IOException(网络问题)和 InterruptedException(线程在等待时被中断)。

示例 2:带进度监控的下载(异步方式)

对于大文件下载,同步方式会阻塞主线程,异步方式更灵活,我们可以在下载过程中更新进度。

HttpClient Java 下载如何实现?-图2
(图片来源网络,侵删)
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
public class AsyncDownloadWithProgress {
    public static void main(String[] args) {
        String fileUrl = "https://speed.hetzner.de/100MB.bin"; // 一个用于测试的大文件
        Path destinationPath = Paths.get("large_file.bin");
        HttpClient httpClient = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(fileUrl))
                .build();
        // 用于跟踪已下载的字节数
        AtomicLong totalBytesDownloaded = new AtomicLong(0);
        System.out.println("开始下载...");
        // 自定义 BodyHandler,用于监控进度
        HttpResponse.BodyHandler<Path> progressHandler = responseInfo -> {
            // 从响应头中获取文件总大小
            long contentLength = responseInfo.headers().firstValueAsLong("Content-Length").orElse(-1);
            System.out.println("文件总大小: " + formatBytes(contentLength));
            return HttpResponse.BodyHandlers.ofFile(destinationPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE)
                    .apply(responseInfo)
                    .thenApply(path -> {
                        // 包装原始的响应流,以便在写入时计算进度
                        return new HttpResponse.BodySubscriber<Path>() {
                            private final HttpResponse.BodySubscriber<Path> downstream = responseInfo.bodyHandler().apply(responseInfo);
                            private final WritableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.WRITE);
                            @Override
                            public CompletionStage<Path> getBody() {
                                return downstream.getBody();
                            }
                            @Override
                            public void onNext(ByteBuffer item) {
                                totalBytesDownloaded.addAndGet(item.remaining());
                                long downloaded = totalBytesDownloaded.get();
                                if (contentLength > 0) {
                                    double percent = (downloaded * 100.0) / contentLength;
                                    System.out.printf("\r下载进度: %.2f%% (%s / %s)", percent, formatBytes(downloaded), formatBytes(contentLength));
                                }
                                try {
                                    channel.write(item);
                                } catch (IOException e) {
                                    throw new RuntimeException(e);
                                }
                            }
                            @Override
                            public void onError(Throwable throwable) {
                                downstream.onError(throwable);
                                try {
                                    channel.close();
                                } catch (IOException e) {
                                    // ignore
                                }
                            }
                            @Override
                            public void onComplete() {
                                downstream.onComplete();
                                try {
                                    channel.close();
                                    System.out.println("\n下载完成!");
                                } catch (IOException e) {
                                    // ignore
                                }
                            }
                        };
                    });
        };
        httpClient.sendAsync(request, progressHandler)
                .thenApply(HttpResponse::body)
                .thenAccept(path -> System.out.println("文件已保存至: " + path.toAbsolutePath()))
                .exceptionally(e -> {
                    System.err.println("下载失败: " + e.getMessage());
                    return null;
                });
        // 主线程可以继续做其他事情,或者等待下载完成
        // 这里我们简单等待一下,因为上面的操作是异步的
        try {
            Thread.sleep(60000); // 等待60秒,让下载有时间完成
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    // 辅助方法:格式化字节大小
    private static String formatBytes(long bytes) {
        if (bytes < 1024) return bytes + " B";
        int exp = (int) (Math.log(bytes) / Math.log(1024));
        char pre = "KMGTPE".charAt(exp - 1);
        return String.format("%.1f %sB", bytes / Math.pow(1024, exp), pre);
    }
}

代码解析:

  1. httpClient.sendAsync(...): 这是异步方法,它立即返回一个 CompletableFuture<HttpResponse<T>>,不会阻塞当前线程。
  2. 自定义 BodyHandler: 我们不再使用简单的 ofFile,而是创建一个自定义的处理器,这个处理器在接收到响应头后,会包装一个 BodySubscriber
  3. 自定义 BodySubscriber: 这是进度监控的核心,我们重写了 onNext 方法,每当数据块(ByteBuffer)从网络到达时,这个方法就会被调用。
    • 我们更新 totalBytesDownloaded 计数器。
    • 我们从 Content-Length 响应头获取总文件大小。
    • 计算并打印下载百分比。
    • 将数据块写入文件通道。
  4. CompletableFuture 链式调用: .thenApply(), .thenAccept(), .exceptionally() 用于处理异步操作的结果,形成清晰的处理流程。
  5. Thread.sleep(): 在这个简单的例子中,主线程需要等待,否则程序可能在下载完成前就退出了,在实际应用中,UI 线程或主业务逻辑线程不会被阻塞。

示例 3:设置超时和重定向

网络请求是不可靠的,设置合理的超时和重定向策略非常重要。

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.time.Duration;
public class DownloadWithTimeout {
    public static void main(String[] args) {
        String fileUrl = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf";
        Path destinationPath = Paths.get("timeout_dummy.pdf");
        // 创建一个带有超时和重定向配置的 HttpClient
        HttpClient httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10)) // 连接超时
                .followRedirects(HttpClient.Redirect.NORMAL) // 自动跟随重定向
                .build();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(fileUrl))
                .timeout(Duration.ofSeconds(30)) // 请求超时
                .build();
        try {
            System.out.println("开始下载(带超时控制)...");
            HttpResponse<Path> response = httpClient.send(
                    request,
                    HttpResponse.BodyHandlers.ofFile(destinationPath)
            );
            if (response.statusCode() == 200) {
                System.out.println("下载成功!文件已保存至: " + destinationPath.toAbsolutePath());
            } else {
                System.err.println("下载失败,HTTP 状态码: " + response.statusCode());
            }
        } catch (HttpTimeoutException e) {
            System.err.println("请求超时: " + e.getMessage());
        } catch (IOException | InterruptedException e) {
            System.err.println("下载过程中发生错误: " + e.getMessage());
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

代码解析:

  1. HttpClient.newBuilder(): 使用构建器模式来创建 HttpClient
    • .connectTimeout(Duration.ofSeconds(10)): 设置建立连接的最长时间。
    • .followRedirects(HttpClient.Redirect.NORMAL): 设置重定向策略。NORMAL 会自动跟随重定向(对于 GET 请求是默认行为),NEVER 不跟随,ALWAYS 即使非 GET 请求也会跟随。
  2. HttpRequest.Builder.timeout(Duration.ofSeconds(30)): 为整个请求(包括连接、发送和接收数据)设置超时时间。
  3. HttpTimeoutException: 专门捕获由超时引起的异常,使错误处理更精确。

总结与最佳实践

功能点 实现方式 说明
基本下载 HttpClient.send() + BodyHandlers.ofFile() 简单直接,适合小文件或脚本。
异步下载 HttpClient.sendAsync() + CompletableFuture 非阻塞,适合 GUI 应用或需要高并发的场景。
进度监控 自定义 BodyHandlerBodySubscriber onNext 回调中计算和更新进度。
超时控制 HttpClient.Builder.connectTimeout()HttpRequest.Builder.timeout() 防止程序因网络问题无限期等待。
错误处理 try-catch (同步) / .exceptionally() (异步) 捕获 IOException, InterruptedException, HttpTimeoutException 等。
大文件处理 总是使用 BodyHandlers.ofFile() 避免将整个文件读入内存,防止 OutOfMemoryError
重定向 HttpClient.Builder.followRedirects() 根据需求配置是否自动跟随重定向。

选择哪种方式取决于您的具体应用场景,对于大多数现代 Java 应用,异步方式(示例2) 是最佳选择,因为它提供了更好的性能和用户体验。

HttpClient Java 下载如何实现?-图3
(图片来源网络,侵删)
分享:
扫描分享到社交APP
上一篇
下一篇