Jersey 作為 JAX-RS (JSR 370) 參考實現,提供了兩種主流文件上傳處理方式:標準 @FormDataParam 多部分表單解析(適合中小文件、表單混合場景)與 InputStream 流式直接讀取(適合大文件、零拷貝、內存受限場景)。本文深度對比兩種方案架構、代碼實現、性能特徵、錯誤處理與生產級最佳實踐。
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-multipart</artifactId>
<version>2.42</version> <!-- 對應 Jersey 版本 -->
</dependency>
<dependency>
<groupId>org.jvnet.mimepull</groupId>
<artifactId>mimepull</artifactId>
<version>1.9.14</version>
</dependency>
// ResourceConfig / Application 子類
register(MultiPartFeature.class); // 必須註冊
// 或 web.xml 初始化參數
<init-param>
<param-name>jersey.config.server.provider.classnames</param-name>
<param-value>org.glassfish.jersey.media.multipart.MultiPartFeature</param-value>
</init-param>
@POST
@Path("/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
public Response uploadFile(
@FormDataParam("file") InputStream fileInputStream,
@FormDataParam("file") FormDataContentDisposition fileMetaData,
@FormDataParam("description") String description) {
String fileName = fileMetaData.getFileName();
long fileSize = fileMetaData.getSize();
String uploadPath = "/data/uploads/" + UUID.randomUUID() + "_" + fileName;
try (OutputStream out = new FileOutputStream(uploadPath)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
return Response.ok(new UploadResult(fileName, fileSize, uploadPath)).build();
} catch (IOException e) {
return Response.serverError().entity("Upload failed: " + e.getMessage()).build();
}
}
關鍵點:同名 @FormDataParam("file") 註解兩次,分別注入 InputStream(文件內容)與 FormDataContentDisposition(元數據:文件名、Content-Type、大小)。
@FormDataParam("files") List fileParts
// 遍歷
for (FormDataBodyPart part : fileParts) {
InputStream is = part.getValueAs(InputStream.class);
FormDataContentDisposition meta = part.getFormDataContentDisposition();
// 處理...
}
Jersey 使用 mimepull 解析,預設超過 2MB 寫入臨時文件。可配置:
@Provider
public class MultiPartConfigProvider implements ContextResolver<MultiPartConfig> {
@Override
public MultiPartConfig getContext(Class<?> type) {
return new MultiPartConfig()
.setMemoryThreshold(1024 * 1024) // 1MB 進內存
.setMaxFileSize(100 * 1024 * 1024) // 100MB 最大文件
.setMaxRequestSize(500 * 1024 * 1024) // 500MB 最大請求
.setFileCleanup(true); // 自動清理臨時文件
}
}
Transfer-Encoding: chunked 或純二進制流。@POST
@Path("/stream-upload")
@Consumes(MediaType.APPLICATION_OCTET_STREAM) // 或 */*
@Produces(MediaType.APPLICATION_JSON)
public Response streamUpload(
@HeaderParam("X-File-Name") String fileName,
@HeaderParam("X-File-Size") long fileSize,
InputStream inputStream) {
// 驗證頭信息
if (fileName == null || fileSize <= 0) {
return Response.status(Response.Status.BAD_REQUEST).entity("Missing headers").build();
}
String savePath = "/data/stream-uploads/" + UUID.randomUUID() + "_" + fileName;
try (OutputStream out = new FileOutputStream(savePath);
BufferedOutputStream bos = new BufferedOutputStream(out, 65536)) {
byte[] buffer = new byte[65536];
long totalRead = 0;
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
totalRead += bytesRead;
// 可選:進度回調、斷點續傳記錄
}
if (totalRead != fileSize) {
log.warn("Size mismatch: expected {}, actual {}", fileSize, totalRead);
}
return Response.ok(new UploadResult(fileName, totalRead, savePath)).build();
} catch (IOException e) {
Files.deleteIfExists(Paths.get(savePath)); // 失敗清理
return Response.serverError().entity("Stream upload failed").build();
}
}
// curl
curl -X POST -H "X-File-Name: big-data.csv" -H "X-File-Size: 1073741824" \
--data-binary @big-data.csv https://host:port/api/stream-upload
// Java HttpClient
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("X-File-Name", fileName)
.header("X-File-Size", String.valueOf(fileSize))
.POST(HttpRequest.BodyPublishers.ofFile(Paths.get(filePath)))
.build();
client.send(request, BodyHandlers.ofString());
| 維度 | Multipart/Form-Data (@FormDataParam) | InputStream 流式 (Chunked) |
|---|---|---|
| 協議標準 | RFC 7578,瀏覽器原生支援 | HTTP 1.1 Chunked Transfer,需客戶端配合 |
| 元數據傳遞 | 表單字段天然攜帶 (文件名、類型、業務字段) | 需自定義 Header 或查詢參數 |
| 解析開銷 | Mimepull 解析,臨時文件/內存緩衝 | 零解析,直接字節流 |
| 內存佔用 | 受閾值控制,大文件落盤 | 極低(僅讀寫緩衝區 64KB) |
| 斷點續傳 | 難實現(整體請求) | 易實現(客戶端分片、服務端 Range/追加寫入) |
| 瀏覽器直傳 | <form enctype="multipart/form-data"> 直接提交 | 需 JavaScript (fetch/xhr) 手動發送流 |
| 進度監聽 | 需前端配合,後端難感知 | 後端讀取字節數天然可計算進度 |
| 錯誤處理 | 解析階段異常、文件級異常分離 | 流讀取異常即整體失敗 |
StorageService,實現 LocalFS、S3、MinIO、HDFS 多後端切換。202 Accepted,異步處理(消息隊列 + Worker),WebSocket/Server-Sent Events 推送進度。Content-Range / Range 標準,或開源庫(tus.io、Resumable.js)。Multipart/Form-Data 是通用標準,適合表單混合、瀏覽器直傳、中小文件;InputStream 流式 是極致性能選擇,適合大文件、斷點續傳、內存受限、異步管道處理。架構建議:網關層統一接入,按文件大小/業務類型路由到不同處理鏈路;存儲層抽象解耦,上層業務無感知。