使用原生 `HttpURLConnection` 構建 `multipart/form-data` 請求,可同時上傳文件與傳遞普通表單參數,無需第三方庫。本文提供兩個生產級工具方法:一種基於 `File` 物件,另一種基於 `byte[]` 數據流,涵蓋邊界生成、請求體組裝、流式寫入、響應處理與資源釋放的完整細節。
--BOUNDARY\r\n Content-Disposition: form-data; name="paramName"\r\n Content-Type: text/plain; charset=UTF-8\r\n \r\n paramValue\r\n --BOUNDARY\r\n Content-Disposition: form-data; name="file"; filename="test.png"\r\n Content-Type: application/octet-stream\r\n \r\n <文件二進制內容>\r\n --BOUNDARY--\r\n
--BOUNDARY\r\n` 開頭,以 `\r\n` 結尾。--BOUNDARY--\r\n` 結束。public static void postMultipart(String actionUrl,
Map<String, String> params,
Map<String, File> files) throws IOException {
String boundary = UUID.randomUUID().toString();
String prefix = "--", lineEnd = "\r\n";
String contentType = "multipart/form-data; boundary=" + boundary;
HttpURLConnection conn = (HttpURLConnection) new URL(actionUrl).openConnection();
conn.setReadTimeout(5000);
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
conn.setRequestProperty("Connection", "keep-alive");
conn.setRequestProperty("Charset", "UTF-8");
conn.setRequestProperty("Content-Type", contentType);
DataOutputStream out = new DataOutputStream(conn.getOutputStream());
// 1. 寫入普通參數
for (Map.Entry<String, String> entry : params.entrySet()) {
StringBuilder sb = new StringBuilder();
sb.append(prefix).append(boundary).append(lineEnd);
sb.append("Content-Disposition: form-data; name=\"").append(entry.getKey()).append("\"").append(lineEnd);
sb.append("Content-Type: text/plain; charset=UTF-8").append(lineEnd);
sb.append("Content-Transfer-Encoding: 8bit").append(lineEnd);
sb.append(lineEnd);
sb.append(entry.getValue()).append(lineEnd);
out.write(sb.toString().getBytes("UTF-8"));
}
// 2. 寫入文件
if (files != null) {
for (Map.Entry<String, File> entry : files.entrySet()) {
StringBuilder sb = new StringBuilder();
sb.append(prefix).append(boundary).append(lineEnd);
sb.append("Content-Disposition: form-data; name=\"file\"; filename=\"").append(entry.getKey()).append("\"").append(lineEnd);
sb.append("Content-Type: application/octet-stream").append(lineEnd);
sb.append(lineEnd);
out.write(sb.toString().getBytes("UTF-8"));
try (FileInputStream in = new FileInputStream(entry.getValue())) {
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
out.write(lineEnd.getBytes());
}
}
// 3. 結束標記
byte[] endData = (prefix + boundary + prefix + lineEnd).getBytes();
out.write(endData);
out.flush();
// 4. 處理響應
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
try (InputStream in = conn.getInputStream()) {
// 讀取響應體...
}
}
out.close();
conn.disconnect();
}
public static String postMultipartBytes(String actionUrl,
Map<String, String> params,
Map<String, byte[]> files) throws Exception {
String boundary = UUID.randomUUID().toString();
String prefix = "--", lineEnd = "\r\n";
String contentType = "multipart/form-data; boundary=" + boundary;
HttpURLConnection conn = (HttpURLConnection) new URL(actionUrl).openConnection();
conn.setReadTimeout(6000);
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
conn.setRequestProperty("Connection", "keep-alive");
conn.setRequestProperty("Charset", "UTF-8");
conn.setRequestProperty("Content-Type", contentType);
DataOutputStream out = new DataOutputStream(conn.getOutputStream());
// 參數部分(同上)
for (Map.Entry<String, String> entry : params.entrySet()) {
StringBuilder sb = new StringBuilder();
sb.append(prefix).append(boundary).append(lineEnd);
sb.append("Content-Disposition: form-data; name=\"").append(entry.getKey()).append("\"").append(lineEnd);
sb.append("Content-Type: text/plain; charset=UTF-8").append(lineEnd);
sb.append("Content-Transfer-Encoding: 8bit").append(lineEnd);
sb.append(lineEnd);
sb.append(entry.getValue()).append(lineEnd);
out.write(sb.toString().getBytes("UTF-8"));
}
// 文件字節數組部分
if (files != null) {
for (Map.Entry<String, byte[]> entry : files.entrySet()) {
StringBuilder sb = new StringBuilder();
sb.append(prefix).append(boundary).append(lineEnd);
sb.append("Content-Disposition: form-data; name=\"pic\"; filename=\"").append(entry.getKey()).append("\"").append(lineEnd);
sb.append("Content-Type: application/octet-stream").append(lineEnd);
sb.append(lineEnd);
out.write(sb.toString().getBytes("UTF-8"));
out.write(entry.getValue()); // 直接寫入 byte[]
out.write(lineEnd.getBytes());
}
}
// 結束標記
byte[] endData = (prefix + boundary + prefix + lineEnd).getBytes();
out.write(endData);
out.flush();
// 響應處理
StringBuilder response = new StringBuilder();
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
try (InputStream in = conn.getInputStream()) {
int ch;
while ((ch = in.read()) != -1) {
response.append((char) ch);
}
}
}
out.close();
conn.disconnect();
return response.toString();
}
| 細節 | 說明 |
|---|---|
| 邊界隨機性 | 使用 `UUID.randomUUID()` 避免與文件內容衝突。 |
| 字符集 | 統一 UTF-8,`getBytes("UTF-8")` 而非平台預設。 |
| 流式寫入 | 文件用 `FileInputStream` 分塊讀寫,避免大文件 OOM。 |
| 內存文件 | `byte[]` 方案適合小文件、加密後數據、內存圖片。 |
| 連接超時 | 設置 `setConnectTimeout` / `setReadTimeout` 防止掛死。 |
| 資源釋放 | `try-with-resources` 管理流,`finally` 關閉連接。 |
| 大文件優化 | 超大文件建議採用 `HttpClient` / `OkHttp` 支援分塊/斷點續傳。 |
原生 `HttpURLConnection` 實現 `multipart/form-data` 雖代碼較繁瑣,但零依賴、可控性強。掌握邊界構建、請求體組裝順序(參數在前、文件在後)、流式寫入與資源釋放,即可應對大多數文件上傳場景。生產環境高併發、大文件、斷點續傳需求建議遷移至 Apache HttpClient 5.x 或 OkHttp 4.x。