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

Java 模擬 POST 表單提交實現圖片上傳:HttpClient 與 HttpURLConnection 兩大方案詳解

     編輯:Java 相關知識

前言

在爬蟲、對接第三方 API、自動化測試等場景中,Java 程序常需模擬瀏覽器表單提交 (multipart/form-data) 上傳圖片。本文對比 Apache HttpClient (推薦) 與原生 HttpURLConnection 兩種主流方案,提供生產級完整代碼,涵蓋邊界生成、流式傳輸、超時控制、進度監聽等關鍵細節。

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

文件上傳請求體由多個 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--

二、方案一:Apache HttpClient 4.5+/5.x (企業級首選)

優勢:自動處理邊界、連接池、重試、攔截器、流式上傳不佔堆內存、支持進度回調。

Maven 依賴

<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>

完整上傳代碼 (HttpClient 5.x)

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

三、方案二:原生 HttpURLConnection (零依賴、輕量場景)

適用場景: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,服務端二次校驗,防篡改與斷點續傳標識。

五、現代替代方案:OkHttp / Spring RestTemplate / WebClient

  • OkHttp:Android/現代 Java 首選,API 簡潔,支持 HTTP/2、連接池、攔截器。RequestBody.create(MediaType.parse("image/png"), file) 搭配 MultipartBody.Builder。
  • Spring RestTemplate:MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); body.add("file", new FileSystemResource(file)); restTemplate.postForObject(url, body, String.class); (底層仍用 HttpClient)。
  • Spring WebClient (WebFlux):響應式非阻塞,大文件上傳不佔用事件循環線程,BodyInserters.fromMultipartData(...)。

六、總結

模擬 POST 上傳圖片的核心是正確構造 multipart/form-data 請求體。強烈推薦 Apache HttpClient 5.x (或 OkHttp),其成熟的連接管理、流式傳輸、攔截器機制能讓你專注於業務邏輯而非造輪子。原生 HttpURLConnection 僅作為理解原理或極度受限環境的備選。無論選擇哪種方案,超時設置、流式讀寫、異常處理、SSL 驗證 是生產可用代碼的四大鐵律。

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