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