You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 06:33:15