【问题标题】:Spring upload non multipart file as a streamSpring将非多部分文件作为流上传
【发布时间】:2017-02-06 11:52:21
【问题描述】:

我正在使用 Spring Boot 1.2.5,我想将原始二进制文件上传到控制器。文件大小可能很大,所以我不想将整个请求保存在内存中,而是流式传输文件,实际上文件是在传输开始时生成的,所以客户端甚至不知道文件的大小。我看到了一个如何使用多部分编码文件上传here 执行类似操作的示例。但是,我不想要一个多部分编码的上传,只是一个原始的字节流。我似乎找不到在春季处理这个用例的方法。

【问题讨论】:

    标签: spring file-upload spring-boot binaryfiles


    【解决方案1】:

    您可以只使用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"));
    }
    

    【讨论】:

    • 为了清楚起见,这个处理程序在上传过程中被调用。这意味着没有数据存储在临时文件中,并且在调用时整个文件尚未在内存中,对吗?
    • @cmaynard 处理请求头后进入该方法。然后,您将获得请求正文的流。您可能有一个 servlet 过滤器,它使用 TeeInputStream 之类的东西来保留自己的请求正文缓冲区,但在默认的 spring-boot、spring-mvc 项目中,您不应该有这样的东西。跨度>
    【解决方案2】:

    我想分享一些可能对某人有所帮助的小发现。

    我使用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()很慢,因为它将整个文件复制到另一个位置,然后删除临时文件,使得完成请求需要两倍的时间。

    我调查并发现您可以禁用弹簧功能并手动处理多部分请求,但有点复杂且容易出错。因此,进一步挖掘,我发现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 的那个,因为它会复制文件并且速度很慢。

    EDIT1:

    我使用HttpServletRequest 测试了该解决方案,但它仍会存储一个临时文件,除非您设置弹簧配置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

    【讨论】:

    • 好吧,尽管您的研究很有趣并且有价值 - 它与手头关于流式上传/非多部分的主题无关
    • 感谢您的回答。我可以使用多部分文件的输入流而不将整个文件保存在任何地方吗?
    【解决方案3】:

    要上传不会阻塞 MVC 请求线程池或占用比 JVM 中更多内存的大文件,您可以使用接受 HttpServletRequest(或 InputStream)然后接收它的组合在CompletableFuture 中高效地使用 NIO。

    这是一个示例控制器,可帮助您入门。在实际场景中使用它之前,您需要确保您写入的任何文件名在执行之前都经过严格验证。

    @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();
        });
      }
    }
    

    【讨论】:

      猜你喜欢
      • 2014-10-31
      • 2017-11-05
      • 2021-05-12
      • 2014-05-01
      • 1970-01-01
      • 2012-04-10
      • 1970-01-01
      • 2015-03-26
      • 1970-01-01
      相关资源
      最近更新 更多