Spring将非多部分文件作为流上传

12

我正在使用Spring Boot 1.2.5,想要上传一个原始二进制文件到控制器。由于文件可能很大,因此我不希望将整个请求保存在内存中,而是要流式传输文件,实际上文件在传输开始时生成,因此客户端甚至不知道文件的大小。我看到了一个类似的示例,它演示了如何使用multipart编码的文件上传here。但是,我不想要multipart编码上传,只想要原始的字节流。我似乎找不到在Spring中处理这种情况的方法。

3个回答

11

您可以直接使用HttpServletRequest中的输入流。
请注意,如果您有任何预处理请求并消耗输入流的过滤器,则此方法可能无法正常工作。

@ResponseBody
@RequestMapping(path="fileupload", method = RequestMethod.POST, consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void fileUpload(HttpServletRequest request) throws IOException {
    Files.copy(request.getInputStream(), Paths.get("myfilename"));
}

1
只是为了明确,这个处理程序在上传仍在进行时被调用。这意味着在调用时没有数据存储在临时文件中,整个文件也不会在内存中。正确吗? - cmaynard
1
@cmaynard 当请求头被处理后,该方法将被调用。然后您会得到一个请求正文的流。可能您会有一个Servlet过滤器,使用类似于“TeeInputStream”的东西来保留自己的请求正文缓冲区,但在默认的Spring Boot、Spring MVC项目中,您不应该有任何这样的东西。 - Magnus

11
我想分享一些小发现,可能会对某些人有所帮助。
我正在使用Spring的MultipartFile来上传大文件,并担心Spring会将内容存储在内存中。因此,我决定使用getInputStream()方法,希望这将直接将文件流式传输到所需位置:
@PostMapping("/upload")
    public ResponseEntity<?> uploadFile(@RequestPart MultipartFile file) throws FileNotFoundException, IOException{

    FileCopyUtils.copy(file.getInputStream(), new FileOutputStream(new File("/storage/upload/", file.getOriginalFilename())));

    return ResponseEntity.ok("Saved");
}

当我用一个2GB的文件测试控制器时,调用控制器方法需要很长时间。因此我进行了调试,并发现Spring/Tomcat在处理文件之前首先将其存储在临时文件夹中。这意味着,当您调用getInputStream()时,它返回一个指向文件系统上存储的文件的FileInputStream,而不是直接从客户端浏览器流式传输。
换句话说,调用FileCopyUtils.copy()很慢,因为它会将整个文件复制到另一个位置,然后删除临时文件,使得完成请求需要两倍的时间。
我调查并发现您可以禁用Spring功能并手动处理多部分请求,但这种方法比较复杂,容易出错。因此,我继续深入研究,发现MultipartFile有一个名为transferTo的方法,它实际上将临时文件移动到所需位置。我测试过,它是瞬间完成的。我的代码变成了这样:
@PostMapping("/upload")
public ResponseEntity<?> uploadFile(@RequestPart MultipartFile file) throws FileNotFoundException, IOException{

    file.transferTo(new File("/storage/upload/", file.getOriginalFilename()));

    return ResponseEntity.ok("Saved");
}

总之,如果你只想将文件上传到特定的目录/文件中,你可以使用这个解决方案,它的速度与手动流式传输文件一样快。

重要提示:有两个transferTo()方法,一个接收Path,另一个接收File。不要使用接收Path的那个,因为它会复制文件并且速度较慢。

编辑1:

我使用HttpServletRequest测试了这个解决方案,但是除非你设置Spring配置spring.servlet.multipart.enabled = false,否则它仍然会存储一个临时文件。使用MultipartHttpServletRequest的解决方案也是如此。

我认为使用我找到的解决方案有三个主要优点:

  1. 它很简单
  2. 它允许你一次处理多个文件,只需在你的控制器方法中添加多个@RequestPart MultipartFile即可
  3. 它允许你轻松地处理带有文件的响应体
public ResponseEntity<?> uploadFile(@RequestPart @Valid MyCustomPOJO pojo, @RequestPart MultipartFile file1, @RequestPart MultipartFile file2, @RequestPart MultipartFile file3)

这是我创建的一个测试项目的URL,用于测试一些概念,包括此概念:

https://github.com/noschang/SpringTester


尽管你的研究很有趣且具有价值,但它与当前关于流式上传/非多部分的话题无关。 - Mistriel
谢谢您提供这样的答案。我能否在不将整个文件保存在任何地方的情况下使用多部分文件的输入流? - ASten
当您使用MultipartFile时,Spring在控制器中会将文件暂存到内存中,这不还存在问题吗? - Ville Miekk-oja
不要使用接收路径的那个,因为它会复制文件并且速度很慢。看源代码,它们似乎需要相同的时间,因为它们都使用FileCopyUtils.copy。 - Heurisko

1
为了上传大文件,不会阻塞MVC请求线程池或使用超出JVM内存的更多内存,您可以使用接受HttpServletRequest(或InputStream)的组合,然后在CompletableFuture中高效地接收它。以下是一个示例控制器,可帮助您入门。在实际场景中使用之前,请确保强烈验证写入的任何文件名。
@RestController
public class TestService {

  @PostMapping("/upload/{filename:.+}")
  public CompletableFuture<ResponseEntity<?>> upload(HttpServletRequest request, @PathVariable("filename") String filename)
      throws ServiceUnavailableException, NotFoundException {

    final int MAX_BUFFER_SIZE = 1024 * 128;

    // TODO: validate 'filename' to ensure it's legal and will be written where you want it
    // to be within the file system. Watch out for the many security gotchas.

    // asynchronously accept the upload

    return CompletableFuture.supplyAsync(() -> {

      try {

        // TODO: Change this to where you want the file to be written
        Path file = Paths.get(filename);

        try (ReadableByteChannel inChannel = Channels.newChannel(request.getInputStream())) {
          try (WritableByteChannel outChannel = Files.newByteChannel(file, CREATE, TRUNCATE_EXISTING, WRITE)) {

            // no way to free a ByteBuffer manually - GC does it
            ByteBuffer buffer = ByteBuffer.allocateDirect(MAX_BUFFER_SIZE);

            while (inChannel.read(buffer) != -1) {
              buffer.flip();
              outChannel.write(buffer);
              buffer.compact();
            }

            // EOF will leave buffer in fill state, flip it and write anything remaining

            buffer.flip();

            while (buffer.hasRemaining()) {
              outChannel.write(buffer);
            }
          }
        }
      } catch (IOException ex) {
        // TODO: log the exception because spring doesn't seem to do that
        throw new ResponseStatusException(INTERNAL_SERVER_ERROR, "Failed to upload the file", ex);
      }

      // upload completed successfully

      return ResponseEntity.ok().build();
    });
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接