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

基於 Jersey 實現 HTTP Multipart 協議解析與文件上傳實戰

     編輯:Java 程式設計入門

什麼是 HTTP Multipart?

multipart/form-data 是 HTTP 協議中用於在單一請求體中傳輸多個獨立數據部分的編碼格式。每個部分由 Content-Disposition 頭標識名稱,可包含各自的 Content-Type。常見場景:文件上傳、混合表單數據(如同時傳輸 JSON 元數據與二進制文件)。

標準請求結構示例

POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="metadata"; filename="meta.json"
Content-Type: application/json

{"userId":123,"tags":["doc","archive"]}
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="report.pdf"
Content-Type: application/pdf

%PDF-1.4...二進制內容...
------WebKitFormBoundary7MA4YWxkTrZu0gW--

關鍵點:boundary 參數定義分隔符,各部分以 --boundary 開頭,結尾以 --boundary-- 結束。

Jersey Multipart 模塊核心 API

需引入依賴(以 Maven 為例):

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-multipart</artifactId>
    <version>2.42</version> <!-- 對應 Jersey 版本 -->
</dependency>

核心類:

  • FormDataMultiPart:構建/解析 multipart 實體的主入口。
  • FormDataBodyPart:單個部分,提供 getName()、getEntityAs(Class)、getContentDisposition()。
  • BodyPart:父接口,通用部分操作。
  • @FormDataParam:參數級注解注入(JAX-RS 2.1+)。

客戶端構建 Multipart 請求

import org.glassfish.jersey.media.multipart.FormDataMultiPart;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

public class MultipartClient {
    private static final Client client = ClientBuilder.newBuilder()
            .register(MultiPartFeature.class)  // 必須註冊!
            .build();
    private static final WebTarget target = client.target("https://api.example.com/upload");

    public static String upload(String xmlHead, String xmlBody) {
        FormDataMultiPart multiPart = new FormDataMultiPart()
                .field("xmlhead", xmlHead, MediaType.APPLICATION_XML_TYPE)
                .field("xmlbody", xmlBody, MediaType.APPLICATION_XML_TYPE);

        // 注意:不需手動設置 Content-Type,MultiPartFeature 會自動添加 boundary
        Response response = target.request()
                .post(Entity.entity(multiPart, MediaType.MULTIPART_FORM_DATA_TYPE));
        
        String result = response.readEntity(String.class);
        response.close();
        multiPart.close();  // 釋放臨時文件資源
        return result;
    }
}

關鍵細節:

  1. 必須註冊 MultiPartFeature,否則序列化失敗。
  2. field(name, value, mediaType) 適用於字符串/對象;文件上傳用 bodyPart = new FormDataBodyPart("file", fileInputStream, MediaType.APPLICATION_OCTET_STREAM_TYPE) 並設置 contentDisposition。
  3. 用完調用 multiPart.close() 釋放臨時文件(大文件會落盤)。

服務端解析 Multipart 請求

方式一:註解注入(推薦,代碼簡潔)

import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.InputStream;

@Path("/upload")
public class UploadResource {

    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    public Response upload(
            @FormDataParam("xmlhead") String xmlHead,
            @FormDataParam("xmlbody") String xmlBody,
            @FormDataParam("file") InputStream fileStream,
            @FormDataParam("file") FormDataContentDisposition fileMeta) {
        
        String fileName = fileMeta.getFileName();
        long fileSize = fileMeta.getSize();
        // 處理文件流...
        
        return Response.ok("上傳成功: " + fileName).build();
    }
}

方式二:手動解析 FormDataMultiPart(靈活控制)


@POST
@Path("/batch")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
public Response batchUpload(FormDataMultiPart multiPart) {
    if (multiPart == null) {
        return Response.status(Response.Status.BAD_REQUEST).entity("請求體為空").build();
    }

    List<PartData> parts = new ArrayList<>();
    for (BodyPart bodyPart : multiPart.getBodyParts()) {
        FormDataBodyPart part = (FormDataBodyPart) bodyPart;
        String name = part.getName();
        String value = part.getEntityAs(String.class);  // 文本部分
        // InputStream stream = part.getEntityAs(InputStream.class); // 文件部分
        
        parts.add(new PartData(name, value));
    }
    multiPart.close(); // 重要!
    return Response.ok(parts).build();
}

實戰案例:雙 XML 協議適配(移動網狀網場景)

業務需求:單請求攜帶 xmlhead(路由元數據)與 xmlbody(業務報文)兩個獨立 XML 部分。

客戶端調用

public class BossClient {
    private static final String HEAD_FIELD = "xmlhead";
    private static final String BODY_FIELD = "xmlbody";

    public String send(String headXml, String bodyXml) {
        FormDataMultiPart mp = new FormDataMultiPart()
                .field(HEAD_FIELD, headXml, MediaType.APPLICATION_XML_TYPE)
                .field(BODY_FIELD, bodyXml, MediaType.APPLICATION_XML_TYPE);
        return webResource.type(MediaType.MULTIPART_FORM_DATA).post(String.class, mp);
    }
}

服務端處理


@POST
@Path("/boss")
@Consumes({MediaType.MULTIPART_FORM_DATA, MediaType.APPLICATION_XML})
@Produces(MediaType.APPLICATION_XML)
public String handleBossRequest(FormDataMultiPart multiPart) {
    if (multiPart == null) throw new BadRequestException("Multipart 數據為空");

    String headXml = null, bodyXml = null;
    for (BodyPart bp : multiPart.getBodyParts()) {
        FormDataBodyPart part = (FormDataBodyPart) bp;
        String fieldName = part.getName().trim();
        String content = part.getEntityAs(String.class);
        
        if (HEAD_FIELD.equalsIgnoreCase(fieldName)) headXml = content;
        else if (BODY_FIELD.equalsIgnoreCase(fieldName)) bodyXml = content;
        else log.warn("忽略未知字段: {}", fieldName);
    }
    multiPart.close();

    // 業務邏輯:解析 headXml 路由,處理 bodyXml 訂購/退訂...
    return buildResponseXml("SUCCESS");
}

實際抓包報文(關鍵字段)

Content-Type: multipart/form-data; boundary=Boundary_1_30911772_1367997277472

--Boundary_1_30911772_1367997277472
Content-Disposition: form-data; name="xmlhead"
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<InterBOSS><Version>0100</Version>...</InterBOSS>
--Boundary_1_30911772_1367997277472
Content-Disposition: form-data; name="xmlbody"
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<InterBOSS><SvcCont><![CDATA[...]]></SvcCont></InterBOSS>
--Boundary_1_30911772_1367997277472--

常見坑與最佳實踐

問題原因解決方案
服務端收到 null / 400未註冊 MultiPartFeature 或 Content-Type 缺少 boundary客戶端註冊 Feature;確保請求頭正確
大文件上傳 OOM默認全量讀入內存配置 jersey.config.multipart.maxFileSize、maxRequestSize;使用流式處理
臨時文件泄漏未調用 multiPart.close()finally 塊中關閉;或使用 try-with-resources (JDK 9+)
中文文件名亂碼Content-Disposition 編碼不一致使用 RFC 5987 編碼:filename*=UTF-8''%E6%96%87%E4%BB%B6.txt

總結

Jersey 的 Multipart 支持成熟且符合 JAX-RS 標準。核心要點:

  1. 客戶端必須註冊 MultiPartFeature。
  2. 優先使用 @FormDataParam 注解注入,代碼更聲明式。
  3. 複雜場景(動態字段、流式處理)手動遍歷 FormDataMultiPart。
  4. 始終關閉 FormDataMultiPart 釋放臨時資源。
  5. 大文件場景配置大小限制,考慮異步/流式上傳架構。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved