1. 引言
文件与网络相关的工作是编程中至关重要的主题之一。几乎任何现代应用都会以某种方式与互联网交互:下载数据、发送请求、获取图片或文档。即便是简单的程序,比如聊天客户端或音乐播放器,也一定会处理资源的下载。
今天我们就以最直观、最“讨喜”的载荷——图片——为例,来看看在 Java 中如何完成这些操作。为什么选它们?
- 立竿见影:如果一切正常,你只需打开下载的文件就能看到图片。结果是即时且易于理解的。
- 通用技能:图片不只是图片,它们是二进制数据,即“原始字节”。学会处理它们,你就能下载任何东西:文档、视频、音乐、压缩包。原理完全相同!
- 贴近实战:从网络下载资源是构建任何现代应用的基础,从简单的壁纸下载器到与社交网络 API 交互的复杂系统都离不开它。
我们将用两种思路来理解:快速的“邮政服务”——通过 URL,以及“专业运输公司”——通过 HttpClient,并配合对响应头、状态码与超时的控制。
2. 最简单的方法:URL 类
Java 中有一个 URL 类,用来表示网络地址。它可以打开连接并获得数据流(类型为 InputStream)。你可以像处理文件一样处理这个流:读取字节、复制到其他流、按文本处理等。
本质上,使用 URL 是从浏览器中的链接到磁盘上的文件的最短路径。
第一个最小示例
假设我们有一个图片链接:
URL url = new URL("https://example.com/image.jpg");
Files.copy(url.openStream(), Path.of("a.jpg"));
就两行代码。这里发生了什么?
- 创建一个 URL 对象,传入图片地址的字符串。
- 调用 openStream() 方法,它会打开网络连接并返回一个 InputStream。
- 通过 Files.copy 方法将该流的内容复制到文件 "a.jpg" 中。
结果:来自互联网的图片被保存到我们的工作目录中。
使用 transferTo 的变体
还有一种把流中的数据导入文件的方法:
InputStream in = new URL("https://example.com/image.jpg").openStream();
in.transferTo(Files.newOutputStream(Path.of("b.jpg")));
这里我们明确地说:拿到流 in,并用 transferTo 方法把所有数据“输送”到写入文件 "b.jpg" 的输出流中。
结果是一样的:图片会出现在磁盘上。
重要细节
这种方式非常简单,但也有一些细节。首先,我们并没有校验链接返回的到底是什么。可能是图片,也可能是带有 404 错误的 HTML 页面。两种情况下文件都会被保存。只有当你用图片查看器打开它时,差异才会显现。
3. 现代方法:HttpClient
从 Java 11 开始,标准库引入了一个新工具——HttpClient。它可以以现代方式发起 HTTP 请求:支持 GET、POST 等方法,管理超时,检查响应头与状态码,处理重定向。
下载图片时这尤其有用:我们可以确保文件确实成功下载,而不是收到了一个错误页面。
HttpClient 的最小示例
URI uri = URI.create("https://example.com/image.jpg"); // URI —— URL 的更现代版本
HttpClient client = HttpClient.newHttpClient(); // 创建 HttpClient 对象
HttpRequest request = HttpRequest.newBuilder(uri).build(); // 创建“请求”对象
HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
Files.write(Path.of("c.jpg"), response.body());
现在一步步来:
- 创建 HttpClient 客户端。
- 为指定地址构造 HttpRequest 请求。
- 发送请求,并以字节数组形式获取响应:HttpResponse.BodyHandlers.ofByteArray()。
- 使用 Files.write 将这些字节写入文件 "c.jpg"。
结果——还是同一张图片,但现在我们还掌握了服务器如何响应的更多信息。
检查响应状态
response 对象有一个 statusCode() 方法。借助它可以确认服务器是否返回了成功的响应:
if (response.statusCode() == 200)
{
Files.write(Path.of("ok.jpg"), response.body());
}
else
{
System.out.println("错误:代码 " + response.statusCode());
}
现在我们就能确定,保存下来的正是期望的内容,而不是一个错误页面。
获取响应头
假设我们想检查内容类型:
String type = response.headers().firstValue("Content-Type").orElse("未知");
System.out.println("内容类型: " + type);
如果服务器返回了 "image/png" 或 "image/jpeg",那它确实是图片;如果是 "text/html",那就是一个危险信号。
超时设置
为避免服务器长时间无响应导致程序“卡死”,可以设置超时:
URI uri = URI.create("https://example.com/image.jpg");
HttpRequest req = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(5))
.build();
如果服务器在 5 秒内没有响应,请求将以错误结束。
处理重定向
有时链接并不直接指向文件,而是先到一个重定向页面。你可以这样开启自动跟随重定向:
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
4. 实用场景
连续下载多张图片
经常需要下载的不止一张,而是几十甚至上百张图片。比如,你要编写一个程序来保存用户头像或商品照片。用 HttpClient 可以通过循环来完成:
var client = HttpClient.newHttpClient();
String[] urls = {
"https://example.com/img1.jpg",
"https://example.com/img2.jpg"
};
for (int i = 0; i < urls.length; i++)
{
var uri = URI.create(urls[i]);
var request = HttpRequest.newBuilder(uri).build();
var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() == 200) {
Files.write(Path.of("img" + i + ".jpg"), response.body());
}
}
检查文件大小
有时了解图片的字节数很有用。服务器可能会在 "Content-Length" 头中给出:
String length = response.headers().firstValue("Content-Length").orElse("?");
System.out.println("大小: " + length + " 字节");
如果没有该响应头,也可以直接取 response.body().length。
在程序中下载并显示图片
有时不仅要下载图片,还要立即显示。可以使用 javax.imageio.ImageIO 库:
InputStream in = new URL("https://example.com/pic.png").openStream();
BufferedImage img = ImageIO.read(in);
System.out.println("宽度: " + img.getWidth() + ", 高度: " + img.getHeight());
这样,我们可以直接在内存中检查图片,甚至无需将其保存到磁盘。
GO TO FULL VERSION