Spring Boot通过MultipartFile
接口简化了文件上传的处理。在Spring MVC中,你可以使用@RequestParam
注解与MultipartFile
一起工作来接收上传的文件。Spring Boot默认配置了文件上传的大小限制,但是你可以通过application.properties
进行自定义配置。
# 单个文件大小限制
spring.servlet.multipart.max-file-size=100MB# 请求中所有文件的总大小限制
spring.servlet.multipart.max-request-size=150MB
1、Java IO vs NIO
Java IO是针对流式数据设计的,每次只能操作一个字节的数据。这让它在处理大文件或进行频繁的文件操作时,性能会相对较低。相反,Java NIO设计用来处理块或缓冲区的数据,一个缓冲区可以包含多个字节的数据,因此在处理大文件或频繁的文件操作时,性能可能更好。
2、文件传输方法性能对比
a、Spring 文件上传提供的方法 MultipartFile#transferTo()
@PostMapping("/upload")
public void upload(@RequestParam("file") MultipartFile file) {File dest = new File("path/to/destination");file.transferTo(dest.toPath());
}
方法内部使用了
java.nio.file.Files#copy(java.io.InputStream, java.nio.file.Path, java.nio.file.CopyOption...)
来实现文件的读写操作,这是NIO API的一部分。然而,这种方式仍然是在IO级别进行操作,并没有使用到NIO中的通道(Channel)和缓冲区(Buffer)机制,因此性能表现并不优秀。
b、FileChannel#transferTo
文件通道(FileChannel
)是一种可以从文件中读取、写入的通道。一般来说,文件通道的性能比Java IO的流更好。transferTo()
方法的存在就是为了提高性能。
@PostMapping("/upload")
public void uploadNio(@RequestParam("file") MultipartFile file) {try (FileOutputStream fos = new FileOutputStream("path/to/destination");FileChannel outChannel = fos.getChannel()) {FileChannel inChannel = ((FileInputStream) file.getInputStream()).getChannel();inChannel.transferTo(0, inChannel.size(), outChannel);}
}
这个方法使用了Java NIO库中的FileChannel
,并且使用到了操作系统的“零拷贝”特性。“零拷贝”可以减少用户空间和内核空间之间的数据拷贝,因此在处理大文件时,它的性能会比org.springframework.web.multipart.MultipartFile#transferTo(java.nio.file.Path)
更好。
c、完整代码及耗时对比如下
@RequestMapping(value = "/upload", method = RequestMethod.POST,consumes = "multipart/form-data")public CommResp handleFileUpload(@RequestParam("file") MultipartFile file,@RequestParam String erp) {Preconditions.checkArgument(StringUtils.isNotBlank(erp), "上传人erp不存在");Preconditions.checkArgument(file != null && !file.isEmpty(), "文件不能为空");//获取原文件名String originalFilename = file.getOriginalFilename();Preconditions.checkArgument(StringUtils.isNotBlank(originalFilename),"文件名称不能为空");//获取文件后缀String suffixName = originalFilename.substring(originalFilename.lastIndexOf("."));// 文件重命名String storagePath = UPLOAD_PATH + "/" + erp + "_" + System.currentTimeMillis() + suffixName;new Thread(() -> {// 实现方式1long startTime = System.currentTimeMillis();try(//创建文件流FileInputStream fis = (FileInputStream)file.getInputStream();FileOutputStream fos = new FileOutputStream(UPLOAD_PATH + "/" + erp + "_1_" + System.currentTimeMillis() + suffixName);//创建通道(通道间传输)FileChannel inChannel = fis.getChannel();FileChannel outChannel = fos.getChannel()){//使用零拷贝上传inChannel.transferTo(0,inChannel.size(),outChannel);}catch (IOException e){log.error("文件上传失败",e);}log.info("elapsed time1: {}", System.currentTimeMillis() - startTime);}).start();// 实现方式2long startTime = System.currentTimeMillis();try{Path targetPath = Paths.get(storagePath);file.transferTo(targetPath);}catch (IOException e){log.error("文件上传失败",e);return CommResp.failure("文件上传失败");}log.info("elapsed time2: {}", System.currentTimeMillis() - startTime);return CommResp.success(new UploadResultBO(originalFilename.replace(suffixName, ""),storagePath));}
PublishController : elapsed time1: 402
PublishController : elapsed time2: 1178
d、进一步优化传输性能思路
- 多线程分块传输
将大文件分割成多个部分,为每个部分分配一个单独的线程进行读写操作。这样可以充分利用多核CPU的计算能力,提高数据处理的并行度。
代码实现:
@PostMapping("/upload")public void uploadNioMultiThread(@RequestParam("file") MultipartFile file) {try (RandomAccessFile raf = new RandomAccessFile("path/to/destination", "rw");FileChannel outChannel = raf.getChannel()) {FileChannel inChannel = ((FileInputStream) file.getInputStream()).getChannel();long size = inChannel.size();long blockSize = size / THREAD_NUM;CountDownLatch latch = new CountDownLatch(THREAD_NUM);for (int i = 0; i < THREAD_NUM; i++) {long start = i * blockSize;long end = (i == THREAD_NUM - 1) ? size : start + blockSize;executorService.submit(() -> {try (FileChannel channel = (FileChannel) Channels.newChannel(file.getInputStream())) {channel.position(start);outChannel.position(start);channel.transferTo(start, end - start, outChannel);} catch (IOException e) {e.printStackTrace();} finally {latch.countDown();}});}latch.await();} catch (Exception e) {e.printStackTrace();}}
- 使用内存映射文件
NIO提供了内存映射文件的支持,可以通过映射到内存的方式来直接操作文件,省去了内核空间和用户空间之间数据复制的开销。
代码实现:
/*** 1、开启输入文件Channel* 2、计算文件大小* 3、循环上传文件内容,每次处理10mb大小的数据块* 4、使用FileChannel.MapMode.READ_ONLY模式,以只读的方式,将输入文件的内容映射到内存中* 5、使用内存映射,将生成的ByteBuffer写入到输出文件Channel中* 6、修改当前位置,以便处理下一个数据块* @param file*/@PostMapping("/upload")public void uploadFileWithMemoryMapped(@RequestParam("file") MultipartFile file) {try (RandomAccessFile raf = new RandomAccessFile("path/to/destination", "rw");FileChannel outChannel = raf.getChannel()) {FileChannel inChannel = ((FileInputStream) file.getInputStream()).getChannel();long fileSize = inChannel.size();long pos = 0L;/* Map the file into memory, and upload it by parts. */while (pos < fileSize) {long limit = Math.min(10485760L, fileSize - pos); // map 10MB at a timeMappedByteBuffer inMappedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY, pos, limit);outChannel.write(inMappedBuf); // This writes data to the filepos += limit;}} catch (IOException e) {// handle exception}}
在处理大型文件传输时,可以考虑同时实现内存映射文件和多线程分片传输,以提高大型文件处理的程序性能。