如何在Jersey中消费嵌套MultiPart?编写接收文件流与JSON的REST端点
嘿,看你已经在客户端把多部分请求的结构搭起来了,我来帮你搞定服务端的REST端点实现,顺便梳理下整个流程里要注意的关键点~
服务端REST端点实现(以Spring Boot为例)
首先咱们得明确:你客户端发送的是嵌套的multipart请求(外层parent套着内层child,child里包含JSON元数据和多个文件流),Spring默认的Multipart解析器只处理顶层的multipart,所以嵌套部分得手动解析。
步骤1:定义JSON元数据的DTO
先把你客户端里someObject对应的DTO写好,方便服务端反序列化JSON:
import com.fasterxml.jackson.annotation.JsonProperty; public class FileMetadata { @JsonProperty("file_group_name") private String fileGroupName; @JsonProperty("upload_time") private long uploadTime; // 这里根据你的someObject字段来定义,记得加getter/setter // 可选:重写toString方便调试 @Override public String toString() { return "FileMetadata{" + "fileGroupName='" + fileGroupName + '\'' + ", uploadTime=" + uploadTime + '}'; } }
步骤2:编写控制器端点
这里用Spring Boot的控制器,结合Apache Commons FileUpload来解析嵌套的multipart流:
import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartResolver; import org.springframework.web.multipart.support.StandardServletMultipartResolver; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.InputStream; import java.util.Iterator; @RestController public class MultiFileUploadController { private final MultipartResolver multipartResolver = new StandardServletMultipartResolver(); @PostMapping(value = "/api/files/upload", consumes = MediaType.MULTIPART_MIXED_VALUE) public String handleNestedMultipartUpload(HttpServletRequest request) throws IOException { // 先判断是否是multipart请求 if (!multipartResolver.isMultipart(request)) { return "Invalid request: not a multipart request"; } MultipartHttpServletRequest multipartRequest = multipartResolver.resolveMultipart(request); Iterator<String> partNames = multipartRequest.getFileNames(); // 遍历顶层的multipart parts while (partNames.hasNext()) { String partName = partNames.next(); String contentType = multipartRequest.getContentType(partName); // 识别嵌套的multipart(就是你客户端里的child) if (contentType != null && contentType.contains(MediaType.MULTIPART_MIXED_VALUE)) { try (InputStream nestedStream = multipartRequest.getInputStream(partName)) { // 解析嵌套的multipart里的JSON和文件流 parseNestedMultipartContent(nestedStream); } } } return "Files and metadata uploaded successfully"; } private void parseNestedMultipartContent(InputStream nestedInputStream) throws IOException { // 用Apache Commons FileUpload解析嵌套的流 org.apache.commons.fileupload.FileItemFactory factory = new org.apache.commons.fileupload.disk.DiskFileItemFactory(); org.apache.commons.fileupload.servlet.ServletFileUpload upload = new org.apache.commons.fileupload.servlet.ServletFileUpload(factory); // 模拟ServletRequest上下文,让FileUpload能解析流 org.apache.commons.fileupload.servlet.ServletRequestContext requestContext = new org.apache.commons.fileupload.servlet.ServletRequestContext() { @Override public InputStream getInputStream() throws IOException { return nestedInputStream; } @Override public String getContentType() { return MediaType.MULTIPART_MIXED_VALUE; } @Override public int getContentLength() { return -1; // 未知长度时返回-1 } @Override public String getCharacterEncoding() { return "UTF-8"; } }; org.apache.commons.fileupload.FileItemIterator itemIterator = upload.getItemIterator(requestContext); while (itemIterator.hasNext()) { org.apache.commons.fileupload.FileItemStream item = itemIterator.next(); try (InputStream itemStream = item.openStream()) { if (item.isFormField()) { // 处理JSON元数据部分 if (MediaType.APPLICATION_JSON_VALUE.equals(item.getContentType())) { String jsonContent = new String(itemStream.readAllBytes()); FileMetadata metadata = new com.fasterxml.jackson.databind.ObjectMapper().readValue(jsonContent, FileMetadata.class); System.out.println("Received metadata: " + metadata); // 这里可以把元数据存数据库或者做其他业务处理 } } else { // 处理文件流部分 String fileName = item.getName() != null ? item.getName() : "unknown-file-" + System.currentTimeMillis(); System.out.println("Processing file: " + fileName); // 示例:把文件流写入本地磁盘 // java.nio.file.Files.copy(itemStream, java.nio.file.Paths.get("/tmp/uploads/" + fileName)); // 或者直接把流传递给业务逻辑处理 } } } } }
步骤3:客户端代码优化建议
给每个BodyPart加上明确的名称和文件名,方便服务端识别和调试:
public void doSomething() { MultiPart parent = new MultiPart(MultiPartMediaTypes.createMixed()); MultiPart child = new MultiPart(MultiPartMediaTypes.createMixed()); // 给JSON元数据part指定名称,方便服务端识别 child.bodyPart(new BodyPart("file_metadata", someObject, MediaType.APPLICATION_JSON_TYPE)); // 给文件流指定名称和文件名 child.bodyPart(new StreamDataBodyPart("file_1", someInputStream, "document.pdf")); child.bodyPart(new StreamDataBodyPart("file_2", someOtherInputStream, "image.png")); parent.bodyPart(child); // 用WebClient发送请求(Spring WebFlux) org.springframework.web.reactive.function.client.WebClient webClient = org.springframework.web.reactive.function.client.WebClient.create("http://localhost:8080"); webClient.post() .uri("/api/files/upload") .contentType(MediaType.MULTIPART_MIXED) .bodyValue(parent) .retrieve() .bodyToMono(String.class) .block(); // 同步调用,异步的话用subscribe() }
关键注意点
- 嵌套Multipart解析:Spring默认不支持嵌套的multipart,所以必须用Apache Commons FileUpload手动解析流,记得在pom.xml(Maven)里加依赖:
<dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency> - 流的关闭:一定要用
try-with-resources自动关闭输入流,避免资源泄漏 - 媒体类型匹配:服务端端点的
consumes必须设置为MediaType.MULTIPART_MIXED_VALUE,和客户端发送的类型一致
内容的提问来源于stack exchange,提问作者Chris
相关产品推荐
相关产品推荐

