HTTP Multipart(multipart/form-data)是文件上傳、混合數據提交的標準協議。本文基於 Jersey 框架,通過一個「XML Head + XML Body」雙部件業務場景,完整演示客戶端構建請求、服務端解析響應的最佳實踐。
POST /resource HTTP/1.1
Content-Type: multipart/form-data; boundary=Boundary_1_30911772_1367997277472
--Boundary_1_30911772_1367997277472
Content-Disposition: form-data; name="xmlhead"
Content-Type: multipart/form-data
...head xml...
--Boundary_1_30911772_1367997277472
Content-Disposition: form-data; name="xmlbody"
Content-Type: multipart/form-data
--Boundary_1_30911772_1367997277472--
關鍵點:
boundary 隨機生成,分隔各部件Content-Disposition: form-data; name="fieldName"Content-Type--boundary-- 結束
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-multipart</artifactId>
<version>2.35</version> <!-- 對應 Jersey 版本 -->
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>2.35</version>
</dependency>
註冊特性:register(MultiPartFeature.class)(客戶端/服務端均需)
import org.glassfish.jersey.client.ClientConfig;
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.WebTarget;
import javax.ws.rs.core.MediaType;
public class MultipartClient {
private static final String HEAD_FIELD = "xmlhead";
private static final String BODY_FIELD = "xmlbody";
private final WebTarget target;
public MultipartClient(String baseUrl) {
ClientConfig config = new ClientConfig().register(MultiPartFeature.class);
Client client = ClientBuilder.newClient(config);
this.target = client.target(baseUrl);
}
public String post(String headXml, String bodyXml) {
FormDataMultiPart multiPart = new FormDataMultiPart()
.field(HEAD_FIELD, headXml, MediaType.MULTIPART_FORM_DATA_TYPE)
.field(BODY_FIELD, bodyXml, MediaType.MULTIPART_FORM_DATA_TYPE);
return target.request()
.post(Entity.entity(multiPart, MediaType.MULTIPART_FORM_DATA), String.class);
}
}
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.List;
@Path("/boss")
public class BossResource {
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_XML)
public String service(FormDataMultiPart multiPart) {
if (multiPart == null) {
throw new BadRequestException("Request multipart is null");
}
List fields = new ArrayList<>();
for (FormDataBodyPart part : multiPart.getBodyParts()) {
String name = part.getName().trim();
String value = part.getEntityAs(String.class);
if (HEAD_FIELD.equalsIgnoreCase(name) || BODY_FIELD.equalsIgnoreCase(name)) {
fields.add(new RequestField(name, value));
} else {
log.warn("Ignored unknown field: {}", name);
}
}
// 業務處理...
return buildResponse(fields);
}
}
| 類 | 用途 |
|---|---|
FormDataMultiPart | 專為 application/x-www-form-urlencoded 和 multipart/form-data 設計,.field(name, value) 便捷添加表單字段 |
MultiPart | 通用 multipart 容器,需手動構建 BodyPart,適用於非表單場景(如 multipart/related、multipart/mixed) |
// 字符串
String xml = part.getEntityAs(String.class);
// 輸入流(大文件流式處理)
InputStream is = part.getEntityAs(InputStream.class);
// 字節數組
byte[] bytes = part.getEntityAs(byte[].class);
// 文件上傳場景
FormDataContentDisposition cd = part.getFormDataContentDisposition();
String fileName = cd.getFileName();
InputStream fileIs = part.getEntityAs(InputStream.class);
// 流式寫入磁盤...
Jersey 內部使用 MultiPartWriter 自動生成唯一 boundary,無需手動干預。每次請求 boundary 不同,符合 RFC 2046。
MultiPartFeature 參數或使用 FileDataBodyPart 臨時文件存儲jersey.config.multipart.maxBodySize 限制總大小,maxFileSize 限制單文件| 問題 | 原因 | 解決 |
|---|---|---|
| 服務端收到 null | 未註冊 MultiPartFeature | ClientConfig/ResourceConfig 均 .register(MultiPartFeature.class) |
| 字段名大小寫不匹配 | HTTP 規範不區分大小寫,但代碼比較區分 | 使用 equalsIgnoreCase 或統一規範 |
| 中文亂碼 | 部件未指定 charset,默認 ISO-8859-1 | 客戶端 .field(name, value, MediaType.APPLICATION_XML_TYPE.withCharset("UTF-8")) |
| 大文件 OOM | 全量讀入內存 | 使用 InputStream 流式處理、臨時文件、限制大小 |
| 邊界解析失敗 | Content-Type 缺少 boundary 參數 | 確保發送 multipart/form-data 而非 application/json |
若項目已遷移 Spring 生態,推薦使用 WebClient + MultipartBodyBuilder:
WebClient client = WebClient.create(baseUrl);
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("xmlhead", headXml);
builder.part("xmlbody", bodyXml);
Mono response = client.post()
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(builder.build()))
.retrieve()
.bodyToMono(String.class);
服務端:@RequestPart("xmlhead") String head, @RequestPart("xmlbody") String body,自動綁定,零依賴 Jersey。
Jersey 處理 Multipart 核心在於:
MultiPartFeature 啟用支持FormDataMultiPart.field() 鏈式構建FormDataMultiPart 參數,遍歷 getBodyParts() 解析理解 RFC 2046 協議結構,配合 Jersey 高級 API,即可輕鬆應對混合數據、文件上傳、複雜表單等企業級集成場景。