首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java HttpURLConnection 實現 multipart/form-data 文件上傳與參數傳遞完整指南

     編輯:Java 相關知識

使用原生 `HttpURLConnection` 構建 `multipart/form-data` 請求,可同時上傳文件與傳遞普通表單參數,無需第三方庫。本文提供兩個生產級工具方法:一種基於 `File` 物件,另一種基於 `byte[]` 數據流,涵蓋邊界生成、請求體組裝、流式寫入、響應處理與資源釋放的完整細節。

一、核心原理:multipart/form-data 結構

--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:隨機唯一字串(UUID),分隔各部分。
  • 每部分以 `--BOUNDARY\r\n` 開頭,以 `\r\n` 結尾。
  • 最後以 `--BOUNDARY--\r\n` 結束。

二、工具方法一:File 物件上傳

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();
}

三、工具方法二:byte[] 數據流上傳(適合內存中圖片/加密數據)

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` 支援分塊/斷點續傳。

五、常見錯誤排查

  1. 400 Bad Request:邊界格式錯誤(少 `--`、少 `\r\n`)、`Content-Type` 缺少 boundary 參數。
  2. 文件名亂碼:文件名含中文需 RFC 5987 編碼(`filename*=UTF-8''%E6%96%87%E4%BB%B6.txt`),或由伺服器端解碼。
  3. 參數丟失:`name` 屬性與伺服器端 `@RequestParam` 不匹配。
  4. 連接重置:未關閉輸出流導致請求體未發送完畢,確保 `out.flush()` 後再讀響應。

六、總結

原生 `HttpURLConnection` 實現 `multipart/form-data` 雖代碼較繁瑣,但零依賴、可控性強。掌握邊界構建、請求體組裝順序(參數在前、文件在後)、流式寫入與資源釋放,即可應對大多數文件上傳場景。生產環境高併發、大文件、斷點續傳需求建議遷移至 Apache HttpClient 5.x 或 OkHttp 4.x。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved