
翻译:https://www.javacodegeeks.com/multipart-data-streaming-with-spring-mvc-and-webflux.html
高效处理大文件上传和下载是现代 Web 应用中的常见需求。传统方法通常会将整个文件缓冲在内存或磁盘上,这在处理大负载时可能导致过度的资源使用和性能瓶颈。为了克服这些限制,Spring 提供了顺序流式处理多部分数据的机制。
在本文中,我们探讨如何在 Spring MVC 和 Spring WebFlux(基于响应式)应用中实现多部分上传和下载的流式处理。
本节展示如何使用 Spring MVC 顺序实现流式多部分上传和流式多部分响应。
在 Spring MVC 中配置多部分处理
首先,我们需要在 application.properties 中配置多部分处理。默认情况下,根据文件大小,Spring Boot 可能会将多部分上传缓冲在内存中,然后再写入磁盘。当处理大文件时,这可能导致内存压力。为了确保文件被顺序流式处理而不被缓冲,我们可以将文件大小阈值设置为零,并定义上传的最大限制。
# 确保多部分文件始终直接写入磁盘
spring.servlet.multipart.file-size-threshold=0
# 定义每次上传允许的最大文件大小
spring.servlet.multipart.max-file-size=10MB
# 定义最大请求大小(一次请求中所有文件 + 表单数据的总和)
spring.servlet.multipart.max-request-size=20MB
属性 spring.servlet.multipart.file-size-threshold=0 确保上传的文件直接写入磁盘,而不是在内存中缓冲。这对于顺序流式处理尤其重要,因为它可以防止大文件消耗过多的堆空间。通过将数据直接从请求输入流流式传输到目标文件,应用程序实现了高效且资源友好的文件上传。
此外,spring.servlet.multipart.max-file-size=10MB 定义了单个上传文件允许的最大大小,而 spring.servlet.multipart.max-request-size=20MB 设置了总请求负载的上限,包括多个文件和表单数据。这些限制保护服务器免于处理过大的文件,有助于保持稳定,并防止上传期间的资源耗尽。
在传统的 Spring MVC 应用中,使用 MultipartFile 上传文件通常会先在内存或磁盘上完全缓冲,然后应用程序再处理它们。对于较小的文件,这可能是可以接受的,但对于较大的负载,它可能变得低效。为了解决这个问题,我们可以将上传的文件直接从请求输入流流式传输到服务器的文件系统。
@Controller
publicclass MvcStreamingUploadController {
privatestaticfinal Logger log = LoggerFactory.getLogger(MvcStreamingUploadController.class);
privatefinal Path uploadRoot = Path.of(System.getProperty("java.io.tmpdir"), "mvc-uploads");
@PostMapping("/upload")
public ResponseEntity<String> streamFileUpload(@RequestPart("file") MultipartFile file) throws IOException {
Files.createDirectories(uploadRoot);
Path targetPath = uploadRoot.resolve(System.currentTimeMillis() + "-" + file.getOriginalFilename());
try (InputStream inputStream = file.getInputStream(); OutputStream outputStream = Files.newOutputStream(targetPath)) {
inputStream.transferTo(outputStream);
}
log.info("文件 [{}] 已成功流式传输到 {}", file.getOriginalFilename(), targetPath);
return ResponseEntity.ok("上传成功: " + file.getOriginalFilename());
}
}
/upload 端点接受一个名为 "file" 的多部分文件参数,创建目标目录(mvc-uploads)如果它不存在,并使用 inputStream.transferTo(outputStream) 将文件内容直接从输入流流式传输到磁盘上的文件。这种方法避免了在内存中缓冲大文件,提高了效率并减少了资源使用。
要顺序流式传输多部分响应,其中服务器一个接一个地推送多个部分,您可以使用 StreamingResponseBody 直接将多部分 MIME 结构写入 HttpServletResponse。通过定义动态边界并将内容类型设置为 multipart/mixed,每个部分可以有效地按顺序传送,而无需一次性将所有文件或数据加载到内存中,这使其成为向客户端发送多个文件或混合内容的理想选择。
@Controller
publicclass MvcStreamingDownloadController {
privatestaticfinal Logger log = LoggerFactory.getLogger(MvcStreamingDownloadController.class);
privatefinal Path uploadRoot = Path.of(System.getProperty("java.io.tmpdir"), "mvc-uploads");
privatestaticfinal String BOUNDARY = "MvcBoundary_" + System.currentTimeMillis();
@GetMapping("/download-multipart")
public StreamingResponseBody downloadMultipart(HttpServletResponse response) throws IOException {
response.setContentType("multipart/mixed; boundary=" + BOUNDARY);
return outputStream -> {
try (BufferedOutputStream bos = new BufferedOutputStream(outputStream);
OutputStreamWriter writer = new OutputStreamWriter(bos)) {
// 要流式传输的两个示例文件列表
Path file1 = uploadRoot.resolve("example-file1.txt");
Path file2 = uploadRoot.resolve("example-file2.txt");
Path[] filesToDownload = {file1, file2};
for (Path filePath : filesToDownload) {
if (!Files.exists(filePath)) continue; // 跳过缺失的文件
// 写入多部分边界和头部
writer.write("--" + BOUNDARY + "\r\n");
writer.write("Content-Disposition: attachment; filename=\"" + filePath.getFileName() + "\"\r\n");
writer.write("Content-Type: text/plain\r\n\r\n");
writer.flush();
// 逐行流式传输文件内容
try (BufferedReader reader = Files.newBufferedReader(filePath)) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.write(System.lineSeparator());
}
}
writer.write("\r\n"); // 分隔部分
writer.flush();
log.info("已向客户端流式传输文件 [{}]", filePath.getFileName());
}
// 写入结束边界
writer.write("--" + BOUNDARY + "--\r\n");
writer.flush();
}
};
}
}
此控制器将两个文件作为 multipart/mixed 响应的一部分顺序流式传输。每个部分以动态边界 BOUNDARY 开始,并包含内容处置和类型头部。文件使用 BufferedReader 逐行读取,BufferedOutputStream 与 OutputStreamWriter 确保高效、缓冲地写入客户端。
结束边界表示多部分响应的结束。这种方法允许高效地发送多个文件,而无需一次性将它们全部加载到内存中。
WebFlux 专为流式处理和背压而构建,在本文中,我们演示了使用 FilePart 和 Flux<DataBuffer> API 实现多部分上传和顺序多部分响应的响应式流式处理。
在响应式应用中,Spring WebFlux 提供了一种非阻塞的方式来处理文件上传和下载。WebFlux 在文件到达时流式传输每个部分,而不是在文件 I/O 时阻塞线程,这使其成为传统阻塞 I/O 可能阻碍可扩展性的大文件或高并发场景的理想选择。使用 FilePart.transferTo(Path),文件直接流式传输到目标位置,而无需在内存中缓冲整个内容。
@Controller
publicclass WebFluxStreamingUploadController {
privatestaticfinal Path UPLOAD_ROOT = Path.of(System.getProperty("java.io.tmpdir"), "webflux-uploads");
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseBody
public Mono<String> streamFileUpload(@RequestPart("file") FilePart filePart) {
return Mono.fromCallable(() -> {
Files.createDirectories(UPLOAD_ROOT);
return UPLOAD_ROOT.resolve(filePart.filename());
}).flatMap(targetPath
-> filePart.transferTo(targetPath)
.thenReturn("上传成功: " + filePart.filename())
);
}
}
此控制器定义了一个用于使用 Spring WebFlux 流式传输文件上传的 /upload 端点。上传的文件以 FilePart 形式接收,如果目标目录 webflux-uploads 不存在,则创建它。使用 Mono.fromCallable,我们以响应式方式确定目标路径,filePart.transferTo(targetPath) 以非阻塞方式将文件内容直接流式传输到磁盘。thenReturn 操作符允许在传输完成后返回确认消息。
在 WebFlux 中,通过使用 Flux<DataBuffer> 作为响应主体,并设置内容类型为 multipart/mixed; boundary=...,流式传输多部分响应是最简单的方法。这种方法允许您通过使用 DataBufferUtils.read 读取文件,依次发出响应的每个部分、边界、头部和文件内容,而无需一次性将所有内容加载到内存中。
@RestController
publicclass WebFluxStreamingDownloadHandler {
privatestaticfinal Logger log = LoggerFactory.getLogger(WebFluxStreamingDownloadHandler.class);
privatefinal Path UPLOAD_ROOT = Path.of(System.getProperty("java.io.tmpdir"), "webflux-uploads");
privatestaticfinal String BOUNDARY = "WebFluxBoundary_" + System.currentTimeMillis();
privatefinal DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
@GetMapping(path = "/download/multipart", produces = "multipart/mixed")
public Mono<Void> streamMultipart(ServerHttpResponse response) {
response.getHeaders().setContentType(MediaType.parseMediaType("multipart/mixed; boundary=" + BOUNDARY));
Flux<DataBuffer> partsFlux;
try {
List<Path> files = Files.list(UPLOAD_ROOT).toList();
partsFlux = Flux.fromIterable(files)
.concatMap(file -> {
String filename = file.getFileName().toString();
// 部分的头部
String header = "--" + BOUNDARY + "\r\n"
+ "Content-Type: application/octet-stream\r\n"
+ "Content-Disposition: attachment; filename=\"" + filename + "\"\r\n\r\n";
DataBuffer headerBuf = bufferFactory.wrap(header.getBytes());
// 内容 Flux<DataBuffer>
Flux<DataBuffer> content = DataBufferUtils.read(
file,
bufferFactory,
4096
);
// 内容之后,产生 CRLF
DataBuffer tail = bufferFactory.wrap("\r\n".getBytes());
return Flux.concat(Mono.just(headerBuf), content, Mono.just(tail));
})
// 所有部分之后,产生结束边界
.concatWith(Mono.just(bufferFactory.wrap(("--" + BOUNDARY + "--\r\n").getBytes())));
} catch (Exception ex) {
partsFlux = Flux.just(bufferFactory.wrap(("错误: " + ex.getMessage()).getBytes()));
}
return response.writeWith(partsFlux)
.doOnError(e -> log.error("流式传输失败", e));
}
}
我们计算了一个 Flux<DataBuffer>,对于每个文件,它发出一个头部缓冲区,然后是文件内容作为 Flux<DataBuffer],使用 DataBufferUtils.read(InputStreamSupplier, factory, bufferSize),然后是一个尾随的 CRLF。concatMap 保留了部分的顺序。最后,我们附加了结束边界。
使用 response.writeWith(partsFlux) 将 DataBuffer 流在可用时写入客户端,允许客户端在处理剩余文件时处理前面的部分。
在本文中,我们探讨了如何在 Spring 中使用 Spring MVC 和 Spring WebFlux 高效地流式传输多部分数据。我们演示了上传和下载的顺序流式传输,展示了 Spring MVC 如何使用 MultipartFile 和 StreamingResponseBody 处理文件传输,以及 WebFlux 如何利用 FilePart 和 Flux<DataBuffer> 进行非阻塞、响应式流式传输。通过将数据直接从请求流式传输到响应,应用程序可以在不耗尽内存的情况下处理大文件,提高性能,并支持高并发场景。