关键词:SpringBoot文件上传、文件上传丢失、MultipartFile、transferTo
本人小白一个,学习java没有多长时间,很多问题可能比较幼稚,分享这些经历主要是想和我一个水平的小白共同交流提高,在这个过程中有什么问题也欢迎留言讨论。
最近在练习SpringBoot文件上传,在调用MultipartFile的transferTo方法的时候,文件保存成功,系统也没有任何报错,但就是在自己指定的位置找不到想要的文件,为了搞清楚我的文件被SpringBoot搞到哪里去了,通过跟踪源码,我发现了一个大坑,具体是啥,且听我慢慢道来。
1. 测试环境搭建
1.1 新建一个SpringBoot项目
1.2 配置项目及代码准备
- 文件上传配置application.properties:
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.file-size-threshold=10MB
- java测试代码
@Slf4j
@RestController
@RequestMapping("/upload")
public class FileUploadController {
@PostMapping(value = "/pic", consumes = "multipart/form-data")
public String uploadPic(MultipartFile file) throws IOException {
log.info("入参信息:{},{},{}", file.getContentType(), file.getSize(), file.getOriginalFilename());
File dest = new File("D://tmp//" + file.getOriginalFilename());
file.transferTo(dest);
return dest.getAbsolutePath();
}
}
- 通过idea自带的http client模拟http文件上传请求:
### 文件post上传
POST http://localhost:8080/upload/pic
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="file"; filename="car.jpg"
Content-Type: application/json
< D://trip/car.jpg
--WebAppBoundary--
1.3 测试(用绝对路径,完美通过)
2. 采坑过程(用相对路径)
在上述代码运行成功后,我想通过相对路径,把文件上传到当前项目的文件夹内,所以对代码进行了修改(把绝对路径去掉了):
@Slf4j
@RestController
@RequestMapping("/upload")
public class FileUploadController {
@PostMapping(value = "/pic", consumes = "multipart/form-data")
public String uploadPic(MultipartFile file) throws IOException {
log.info("入参信息:{},{},{}", file.getContentType(), file.getSize(), file.getOriginalFilename());
// File dest = new File("D://tmp//" + file.getOriginalFilename());
File dest = new File(file.getOriginalFilename());
file.transferTo(dest);
return dest.getAbsolutePath();
}
}
测试结果:可以正常返回,但是对应的位置,没有文件,也不知道文件去哪里了!!!
3. 原因分析
想知道文件被SpringBoot搞到哪里去了,只能老老实实的跟踪一下源码了:Debug启动 --> 打断点 --> 上传文件 --> 跳转到兴趣点
- 进入到transferTo方法内
- 继续进入write方法:
- write方法内执行了对上传文件的拷贝操作,但是他居然在一个location变量指定的目录内新建了一个文件
- 执行完这个方法,到location的位置,果然发现了久违的文件!!!
- 虽然看到了我的文件,但是我不甘心,这个怎么能这样设计呢,我给你指定了位置,你把我文件放在了一个新的位置,还不告诉我,躲猫猫呢!!!带着这样的疑问,我继续跟踪代码,回到transferTo方法内部:
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
// 在这个代码内部,我们看到了,如果判断我们自己new的File是使用的相对路径,
// spring会在location临时新建一个文件,把上传的文件放在临时位置
this.part.write(dest.getPath());
// 上边的代码已经把文件保存了,那这个代码是干什么的呢?
if (dest.isAbsolute() && !dest.exists()) {
// Servlet 3.0 Part.write is not guaranteed to support absolute file paths:
// may translate the given path to a relative location within a temp dir
// (e.g. on Jetty whereas Tomcat and Undertow detect absolute paths).
// At least we offloaded the file from memory storage; it'll get deleted
// from the temp dir eventually in any case. And for our user's purposes,
// we can manually copy it to the requested location as a fallback.
FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest.toPath()));
}
}
- 由于if条件判断结果是false,程序不能自动运行,带着上边的疑问,我手动运行了一下if语句里边的代码,居然在我指定的位置看到了我的文件:
- 至此,原因已经清晰了(我认为):
- 我们在Controller里new的File文件,压根没有传递过来,他们只用了文件名;
- 如果文件名不包含绝对路径,他会在location中新建一个文件,把上传的文件放在location中;
- 按照Spring的设计,b只是临时存储,然后通过FileCopyUtils.copy()方法,把文件从临时位置拷贝到用户定义的位置,但是有可能把if逻辑判断搞错了,应该是if (!dest.isAbsolute() && !dest.exists())。看似分析的很有道理,但是人家是不是这个思路设计的呢?
4. GitHub沟通过程
我觉得有可能是个bug,所以将疑问提在git上,经过了漫长的等待,有了回复:https://github.com/spring-projects/spring-framework/issues/27079。
但从回复结果来看,当用相对路径进行文件存储时,他们故意将文件存储在了一个临时位置,他建议我用绝对路径。最终回复结果并没有解决我的最大疑问:如果用户使用的是一个相对路径,为什么不给一个友好提示就把文件偷梁换柱弄到了一个他们指定的临时位置了呢?作为一个小白还是有点心虚,所以也就没再追问。
在poutsma的回复中,提到了一个#09822的issus,这里边把我认为是bug的代码的来龙去脉说的很清楚。链接:https://github.com/spring-projects/spring-framework/issues/19822
我再次跟踪了一下源码,把这个巨坑的临时位置共享出来(***是一些随机数字或者和用户名相关的路径):
// linux
location = /tmp/tomcat.***/work/Tomcat/localhost/ROOT
// windows
location = C:\Users\***\AppData\Local\Temp\tomcat.***\work\Tomcat\localhost\ROOT
5. 解决方案
既然人家也说了,就是这样设计的(突然想起抖音里的一个段子),那只好接受了。那我还想文件上传怎么办呢:
1. 用绝对路径(废话。。。):
就像我最开始演示的那样,给个绝对路径就得了。但是也有一个点需要注意,开发是在windows下,我们如果是真实的项目,是运行在linux系统下,这个时候就需要先判断一下运行环境,基于环境的不同,确定是否需要带盘符(比如D://xxx或者//xxx)。
2. 用相对路径,在源代码中找替代方案(5.1以后):
如下图所示,transferTo有个重载方法,参数类型是Path,代码就一行,基于直觉判断,这个方法靠谱。验证后发现确实有效,但注意,这个方法是5.1以后才有的,如果项目是5.1以前的版本,怎么办呢!!!还用想吗,直接用方法里边的代码呗。。。反正就一行,还是调用了人家的工具类。。。
上代码(Path这个接口很有意思,也值得仔细研究一下):
@PostMapping(value = "/video", consumes = "multipart/form-data")
public String uploadVideo(MultipartFile file) throws IOException {
String uploadFileName = file.getOriginalFilename();
FileSystem fileSystem = FileSystems.getDefault();
Path path = fileSystem.getPath("file", uploadFileName);
Path parent = path.getParent();
File filePath = parent.toFile();
if (!filePath.exists()) {
filePath.mkdirs();
}
File newFile = path.toFile();
file.transferTo(path);
return newFile.getAbsolutePath();
}
3. 在poutsma 的回复中他提到“override the configuration in your container”,肯定也可以,只是我是个小白,不知道如何操作!
6. 总结
没啥好总结的,说点废话吧。在刚开始学java的时候,遇到很多问题都是首先问百度、谷哥,有的时候,他们也不是万能的,还得积极学习源码,遇到问题养成从源码中找解决方案的习惯。这段时间通过阅读源码,确实给自己带来了不少收货,别的不说,人家开发好的工具类,多用几个放在自己的代码里,立马逼格上升一个档次。