我正在使用Spring Boot 1.2.5,想要上传一个原始二进制文件到控制器。由于文件可能很大,因此我不希望将整个请求保存在内存中,而是要流式传输文件,实际上文件在传输开始时生成,因此客户端甚至不知道文件的大小。我看到了一个类似的示例,它演示了如何使用multipart编码的文件上传here。但是,我不想要multipart编码上传,只想要原始的字节流。我似乎找不到在Spring中处理这种情况的方法。
我正在使用Spring Boot 1.2.5,想要上传一个原始二进制文件到控制器。由于文件可能很大,因此我不希望将整个请求保存在内存中,而是要流式传输文件,实际上文件在传输开始时生成,因此客户端甚至不知道文件的大小。我看到了一个类似的示例,它演示了如何使用multipart编码的文件上传here。但是,我不想要multipart编码上传,只想要原始的字节流。我似乎找不到在Spring中处理这种情况的方法。
您可以直接使用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"));
}
@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");
}
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
的那个,因为它会复制文件并且速度较慢。
编辑1:
我使用HttpServletRequest
测试了这个解决方案,但是除非你设置Spring配置spring.servlet.multipart.enabled = false
,否则它仍然会存储一个临时文件。使用MultipartHttpServletRequest
的解决方案也是如此。
我认为使用我找到的解决方案有三个主要优点:
@RequestPart MultipartFile
即可public ResponseEntity<?> uploadFile(@RequestPart @Valid MyCustomPOJO pojo, @RequestPart MultipartFile file1, @RequestPart MultipartFile file2, @RequestPart MultipartFile file3)
这是我创建的一个测试项目的URL,用于测试一些概念,包括此概念:
@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();
});
}
}