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

如果您仍在使用 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();
}
}
}
}
代码解析:
HttpClient.newHttpClient(): 创建一个默认配置的HttpClient实例。HttpRequest.newBuilder().uri(...).build(): 构建一个GET请求(GET是默认方法)。HttpResponse.BodyHandlers.ofFile(destinationPath): 这是关键部分,它告诉HttpClient将响应体(即文件内容)直接写入到指定的Path对象代表的文件中,这种方式非常高效,因为它利用了 NIO 的零拷贝特性,内存占用极低。httpClient.send(...): 发送请求并阻塞,直到收到响应。try-catch: 处理可能发生的IOException(网络问题)和InterruptedException(线程在等待时被中断)。
示例 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);
}
}
代码解析:
httpClient.sendAsync(...): 这是异步方法,它立即返回一个CompletableFuture<HttpResponse<T>>,不会阻塞当前线程。- 自定义
BodyHandler: 我们不再使用简单的ofFile,而是创建一个自定义的处理器,这个处理器在接收到响应头后,会包装一个BodySubscriber。 - 自定义
BodySubscriber: 这是进度监控的核心,我们重写了onNext方法,每当数据块(ByteBuffer)从网络到达时,这个方法就会被调用。- 我们更新
totalBytesDownloaded计数器。 - 我们从
Content-Length响应头获取总文件大小。 - 计算并打印下载百分比。
- 将数据块写入文件通道。
- 我们更新
CompletableFuture链式调用:.thenApply(),.thenAccept(),.exceptionally()用于处理异步操作的结果,形成清晰的处理流程。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();
}
}
}
}
代码解析:
HttpClient.newBuilder(): 使用构建器模式来创建HttpClient。.connectTimeout(Duration.ofSeconds(10)): 设置建立连接的最长时间。.followRedirects(HttpClient.Redirect.NORMAL): 设置重定向策略。NORMAL会自动跟随重定向(对于 GET 请求是默认行为),NEVER不跟随,ALWAYS即使非 GET 请求也会跟随。
HttpRequest.Builder.timeout(Duration.ofSeconds(30)): 为整个请求(包括连接、发送和接收数据)设置超时时间。HttpTimeoutException: 专门捕获由超时引起的异常,使错误处理更精确。
总结与最佳实践
| 功能点 | 实现方式 | 说明 |
|---|---|---|
| 基本下载 | HttpClient.send() + BodyHandlers.ofFile() |
简单直接,适合小文件或脚本。 |
| 异步下载 | HttpClient.sendAsync() + CompletableFuture |
非阻塞,适合 GUI 应用或需要高并发的场景。 |
| 进度监控 | 自定义 BodyHandler 和 BodySubscriber |
在 onNext 回调中计算和更新进度。 |
| 超时控制 | HttpClient.Builder.connectTimeout() 和 HttpRequest.Builder.timeout() |
防止程序因网络问题无限期等待。 |
| 错误处理 | try-catch (同步) / .exceptionally() (异步) |
捕获 IOException, InterruptedException, HttpTimeoutException 等。 |
| 大文件处理 | 总是使用 BodyHandlers.ofFile() |
避免将整个文件读入内存,防止 OutOfMemoryError。 |
| 重定向 | HttpClient.Builder.followRedirects() |
根据需求配置是否自动跟随重定向。 |
选择哪种方式取决于您的具体应用场景,对于大多数现代 Java 应用,异步方式(示例2) 是最佳选择,因为它提供了更好的性能和用户体验。

