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-- 結束。
需引入依賴(以 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+)。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;
}
}
關鍵細節:
MultiPartFeature,否則序列化失敗。field(name, value, mediaType) 適用於字符串/對象;文件上傳用 bodyPart = new FormDataBodyPart("file", fileInputStream, MediaType.APPLICATION_OCTET_STREAM_TYPE) 並設置 contentDisposition。multiPart.close() 釋放臨時文件(大文件會落盤)。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();
}
}
@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();
}
業務需求:單請求攜帶 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 標準。核心要點:
MultiPartFeature。@FormDataParam 注解注入,代碼更聲明式。FormDataMultiPart。FormDataMultiPart 釋放臨時資源。