MultipartFile的transferTo()方法详解
背景
我们在编写Spring Boot应用中,常会遇到文件上传问题,Spring Boot Web提供了MutipartFile的文件支持,具体和File的区别可自行上网搜索查阅。
问题
使用过程中,大部分同学可能会遇到当调用的tansferTo()方法后,再次获取file.getInputStream()方法时,就会报临时文件异常,如:
2022-12-16 10:59:41.971 ERROR 23000 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
java.io.FileNotFoundException: C:\Users\lenovo\AppData\Local\Temp\tomcat.8080.3170522581386951881\work\Tomcat\localhost\ROOT\upload_657023da_e3c3_461e_ae98_639e45ed79cd_00000000.tmp (系统找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method) ~[na:1.8.0_322]
at java.io.FileInputStream.open(FileInputStream.java:195) ~[na:1.8.0_322]
at java.io.FileInputStream.<init>(FileInputStream.java:138) ~[na:1.8.0_322]
at org.apache.tomcat.util.http.fileupload.disk.DiskFileItem.getInputStream(DiskFileItem.java:198) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.apache.catalina.core.ApplicationPart.getInputStream(ApplicationPart.java:100) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile.getInputStream(StandardMultipartHttpServletRequest.java:254) ~[spring-web-5.3.22.jar:5.3.22]
at com.tanwei.spring.app.controllers.FileController.file(FileController.java:29) ~[classes/:na]
FileNotFoundException异常不难理解,就是文件找不到了?也就是说tansferTo()可能在传输完成后把临时文件删除了,这是肯定的,但是答案只能说是对一半,我们将一步一步的进行源码分析
源码分析
调用tansferTo()方法,Spring Boot Web默认是调用StandardMultipartHttpServletRequest.StandardMultipartFile.tansferTo()方法,如下所示:
public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {
// more code..
private static class StandardMultipartFile implements MultipartFile, Serializable {
// more code..
public void transferTo(File dest) throws IOException, IllegalStateException {
this.part.write(dest.getPath());
if (dest.isAbsolute() && !dest.exists()) {
FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest.toPath()));
public void transferTo(Path dest) throws IOException, IllegalStateException {
FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest));
}
我们主要看一下.tansferTo(File dest)这个方法里的this.part.write(dest.getPath());代码,这里的part实现是ApplicationPart,如下所示:
//
// Source code
public class ApplicationPart implements Part {
// more code ...
public void write(String fileName) throws IOException {
// 构建一个需要存储的文件
File file = new File(fileName);
// 判断文件的地址是否是一个绝对路径地址,形如D://x.txt,返回true
if (!file.isAbsolute()) {
// 如果不是一个绝对路径地址,则在this.location下创建
// this.location是一个临时文件对象,地址(C:\xx\Temp\tomcat.8080.3170522581386951881\work\Tomcat\localhost\ROOT)
file = new File(this.location, fileName);
try {
this.fileItem.write(file);
} catch (Exception var4) {
throw new IOException(var4);
// more code ...
}
this.fileItem.write(file);这行代码是主要的核心代码,我们继续跟进去查看一下具体做了什么,如下所示:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
package org.apache.tomcat.util.http.fileupload.disk;
// imports ...
public class DiskFileItem implements FileItem {
// more code ...
public void write(File file) throws Exception {
// 判断文件项是否缓存在内存中的,这里我们没设置,一般都是存在上面的临时磁盘中
if (this.isInMemory()) {
// more code..
} else {
// 主要看一下这个代码块
// 获取文件项的存储位置,即你上传的文件在磁盘上的临时文件
File outputFile = this.getStoreLocation();
if (outputFile == null) {
throw new FileUploadException("Cannot write uploaded file to disk!");
// 获取文件长度
this.size = outputFile.length();
if (file.exists() && !file.delete()) {
throw new FileUploadException("Cannot write uploaded file to disk!");
// 这里至关重要
// 之所以不能再调用file.getInputStream()方法,就是在这
// fileA.renameTo(fileB)方法:
// 1) 当fileA文件信息(包含文件名、文件路径)与fileB全部相同时,只是单纯的重命名
// 2) 当fileA文件信息(特别是文件路径)与fileB不一致时,则存在重命名和剪切,这里的剪切就会把临时文件删除,并将文件复制到fileB位置
// 所以,在调用file.getInputStream()时,file获取的还是原始的文件位置,调用transerTo()方法后(其实调用了renameTo()),原始文件已经不存在了
// 故而抛出FileNotFoundException异常
if (!outputFile.renameTo(file)) {
BufferedInputStream in = null;
BufferedOutputStream out = null;
try {
in = new BufferedInputStream(new FileInputStream(outputFile));
out = new BufferedOutputStream(new FileOutputStream(file));
IOUtils.copy(in, out);
out.close();
} finally {
IOUtils.closeQuietly(in);