在爬蟲、對接第三方 API、自動化測試等場景中,Java 程序常需模擬瀏覽器表單提交 (multipart/form-data) 上傳圖片。本文對比 Apache HttpClient (推薦) 與原生 HttpURLConnection 兩種主流方案,提供生產級完整代碼,涵蓋邊界生成、流式傳輸、超時控制、進度監聽等關鍵細節。
文件上傳請求體由多個 Part 組成,每個 Part 以 --boundary 分隔,包含 Header 與 Body:
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 12345
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="avatar.png"
Content-Type: image/png
<二進制圖片數據>
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="userId"
1001
------WebKitFormBoundary7MA4YWxkTrZu0gW--
優勢:自動處理邊界、連接池、重試、攔截器、流式上傳不佔堆內存、支持進度回調。
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3.1</version>
</dependency>
<!-- 或 4.x 版本 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.io.entity.FileEntity;
import org.apache.hc.core5.http.io.entity.MultipartEntityBuilder;
import org.apache.hc.core5.http.io.entity.ProgressEntity;
import org.apache.hc.core5.util.Timeout;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
public class HttpClientFileUpload {
public static void upload(String url, File imageFile, String userId) throws IOException {
// 1. 創建客戶端 (生產環境建議單例複用,配置連接池)
try (CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionTimeToLive(30, TimeUnit.SECONDS)
.evictIdleConnections(60, TimeUnit.SECONDS)
.build()) {
// 2. 構建 Multipart 請求實體
MultipartEntityBuilder builder = MultipartEntityBuilder.create()
.setCharset(StandardCharsets.UTF_8)
.setBoundary("----CustomBoundary" + System.currentTimeMillis()) // 可選,自動生成更安全
.addBinaryBody(
"file", // 表單字段名
imageFile, // 文件對象 (流式讀取,不載入內存)
ContentType.create(getMimeType(imageFile.getName())), // 自動推斷 MIME
imageFile.getName() // 文件名
)
.addTextBody("userId", userId, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8))
// 可添加更多字段: .addTextBody("token", "xxx")
// 3. (可選) 進度監聽 - 需包裝實體
// ProgressEntity progressEntity = new ProgressEntity(builder.build(), progress ->
// System.out.printf("上傳進度: %.2f%%%n", progress.getTransferredBytes() * 100.0 / progress.getTotalBytes())
// );
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(builder.build()); // 或 progressEntity
// 4. 執行請求並處理響應
try (var response = httpClient.execute(httpPost)) {
int status = response.getCode();
String body = new String(response.getEntity().getContent().readAllBytes(), StandardCharsets.UTF_8);
if (status == 200) {
System.out.println("✅ 上傳成功: " + body);
} else {
System.err.println("❌ 上傳失敗 [" + status + "]: " + body);
}
}
}
}
// 簡易 MIME 類型推斷
private static String getMimeType(String fileName) {
String lower = fileName.toLowerCase();
if (lower.endsWith(".png")) return "image/png";
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
if (lower.endsWith(".gif")) return "image/gif";
if (lower.endsWith(".webp")) return "image/webp";
return "application/octet-stream";
}
public static void main(String[] args) throws IOException {
File testFile = new File("test.png");
if (!testFile.exists()) {
// 創建一個 100x100 紅色測試圖片
javax.imageio.ImageIO.write(
new java.awt.image.BufferedImage(100, 100, java.awt.image.BufferedImage.TYPE_INT_RGB),
"png", testFile
);
}
upload("https://httpbin.org/post", testFile, "user_12345");
}
}
適用場景:Android 早期版本、極度精簡環境、不想引入第三方 JAR。缺點:需手動拼裝邊界、易出錯、無連接池、大文件易 OOM (需分塊寫入)。
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
public class UrlConnectionFileUpload {
public static void upload(String urlStr, File file, String userId) throws IOException {
String boundary = "----Boundary" + UUID.randomUUID().toString().replace("-", "");
String lineEnd = "\r\n";
String twoHyphens = "--";
HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
conn.setRequestProperty("Connection", "Keep-Alive");
conn.setRequestProperty("Charset", "UTF-8");
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
// 設置超時 (生產必須)
conn.setConnectTimeout(10_000);
conn.setReadTimeout(60_000);
try (DataOutputStream dos = new DataOutputStream(conn.getOutputStream())) {
// --- Part 1: 文件 ---
dos.writeBytes(twoHyphens + boundary + lineEnd);
dos.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"" + file.getName() + "\"" + lineEnd);
dos.writeBytes("Content-Type: " + getMimeType(file.getName()) + lineEnd);
dos.writeBytes(lineEnd);
// 流式寫入文件 (避免大文件 OOM)
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
dos.write(buffer, 0, len);
}
}
dos.writeBytes(lineEnd);
// --- Part 2: 普通字段 ---
dos.writeBytes(twoHyphens + boundary + lineEnd);
dos.writeBytes("Content-Disposition: form-data; name=\"userId\"" + lineEnd);
dos.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd);
dos.writeBytes(lineEnd);
dos.writeBytes(userId);
dos.writeBytes(lineEnd);
// --- 結束邊界 ---
dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
dos.flush();
}
// 讀取響應
int responseCode = conn.getResponseCode();
try (InputStream is = responseCode == 200 ? conn.getInputStream() : conn.getErrorStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
System.out.println("響應 [" + responseCode + "]: " + sb);
}
}
private static String getMimeType(String fileName) { /* 同 HttpClient 版本 */ return "application/octet-stream"; }
public static void main(String[] args) throws IOException {
upload("https://httpbin.org/post", new File("test.png"), "user_12345");
}
}
| 維度 | 建議措施 |
|---|---|
| 大文件上傳 | HttpClient 默認流式傳輸;HttpURLConnection 必須分塊寫入 (示例已實現)。超大文件 (>100MB) 採用分片上傳 + 斷點續傳。 |
| 超時控制 | 設置連接超時、讀取超時、請求總超時。HttpClient 5.x 用 RequestConfig。 |
| 重試策略 | HttpClient 配置 HttpRequestRetryExecutor (僅冪等請求 GET/HEAD/OPTIONS,POST 上傳通常不自動重試)。 |
| SSL/HTTPS | 生產環境必須驗證證書。測試環境可信任所有證書 (慎用):SSLContextBuilder.create().loadTrustMaterial(null, (x, y) -> true).build()。 |
| 認證授權 | Header 添加 Token: httpPost.setHeader("Authorization", "Bearer " + token)。 |
| 進度條/取消 | HttpClient 5.x 使用 ProgressEntity 包裝實體;HttpURLConnection 需自定義 FilteredOutputStream 統計寫入字節。 |
| 文件校驗 | 上傳前客戶端計算 MD5/SHA256,服務端二次校驗,防篡改與斷點續傳標識。 |
RequestBody.create(MediaType.parse("image/png"), file) 搭配 MultipartBody.Builder。MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); body.add("file", new FileSystemResource(file)); restTemplate.postForObject(url, body, String.class); (底層仍用 HttpClient)。BodyInserters.fromMultipartData(...)。模擬 POST 上傳圖片的核心是正確構造 multipart/form-data 請求體。強烈推薦 Apache HttpClient 5.x (或 OkHttp),其成熟的連接管理、流式傳輸、攔截器機制能讓你專注於業務邏輯而非造輪子。原生 HttpURLConnection 僅作為理解原理或極度受限環境的備選。無論選擇哪種方案,超時設置、流式讀寫、異常處理、SSL 驗證 是生產可用代碼的四大鐵律。