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(() -> {
// 实现方式1
long 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();
// 实现方式2
long 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 time
MappedByteBuffer inMappedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY, pos, limit);
outChannel.write(inMappedBuf); // This writes data to the file
pos += limit;
}
} catch (IOException e) {
// handle exception
}
}
在处理大型文件传输时,可以考虑同时实现内存映射文件和多线程分片传输,以提高大型文件处理的程序性能。