浅谈MultipartFile中transferTo方法的坑
前言:最近用SpringBoot写文件上传功能,使用参数绑定之后确实是非常的方便了。
但是,项目部署就出现了问题,搞得我一脸懵逼。
后来,才发现是因为我使用了相对路径导致的,这个绝对是一个坑人的地方,不过也说明需要学习的东西还有很多!
案例再现
@PostMapping("/uploadFile") public String uploadImg(@RequestParam("file") MultipartFile file, @RequestParam("equipmentId") String equipmentId) { String baseDir = "./imgFile"; // 这里不能直接使用相对路径 if (!file.isEmpty()) { String name = file.getOriginalFilename(); String prefix = name.lastIndexOf(".") != -1 ? name.substring(name.lastIndexOf(".")) : ".jpg"; String path = UUID.randomUUID().toString().replace("-", "") + prefix; try { // 这里代码都是没有问题的 File filePath = new File(baseDir, path); // 第一次执行代码时,路径是不存在的 logger.info("文件保存路径:{},是否存在:{}", filePath.getParentFile().exists(), filePath.getParent()); if (!filePath.getParentFile().exists()) { // 如果存放路径的父目录不存在,就创建它。 filePath.getParentFile().mkdirs(); } // 如果路径不存在,上面的代码会创建路径,此时路径即已经创建好了 logger.info("文件保存路径:{},是否存在:{}", filePath.getParentFile().exists(), filePath.getParent()); // 此处使用相对路径,似乎是一个坑! // 相对路径:filePath // 绝对路径:filePath.getAbsoluteFile() logger.info("文件将要保存的路径:{}", filePath.getPath()); file.transferTo(filePath); logger.info("文件成功保存的路径:{}", filePath.getAbsolutePath()); return "上传成功"; } catch (Exception e) { logger.error(e.getMessage()); } } return "上传失败"; }
我在日志中打印了路径的位置,显示是没有问题,当时一旦执行到file.transferTo(filePath);就会产生一个FileNotFoundException,但是我前面的代码是执行了,并且创建了一个文件夹的。
Postman测试截图
日志输出
2020-11-27 10:15:06.519 INFO 5200 --- [nio-8080-exec-1] r.controller.LearnController : 文件保存路径:false,是否存在:.\imgFile
2020-11-27 10:15:06.521 INFO 5200 --- [nio-8080-exec-1] r.controller.LearnController : 文件保存路径:true,是否存在:.\imgFile
2020-11-27 10:15:06.521 INFO 5200 --- [nio-8080-exec-1] r.controller.LearnController : 文件将要保存的路径:.\imgFile\684918a520684801b658c85a02bf9ba5.jpg
2020-11-27 10:15:06.522 ERROR 5200 --- [nio-8080-exec-1] r.controller.LearnController : java.io.FileNotFoundException: C:\Users\Alfred\AppData\Local\Temp
\tomcat.8080.2388870592947355119\work\Tomcat\localhost\ROOT\.\imgFile\684918a520684801b658c85a02bf9ba5.jpg (系统找不到指定的路径。)
注意: 这里虽然没有什么头绪,当时观察日志可以发现,程序试图将文件保存到一个很奇怪的目录下,当是这个目录和前面那个filePath已经没有关系了,这里是一个疑点!
执行之后代码所在目录下面已经创建了一个imgFile目录
imgFile文件夹中是空的,因为执行transferTo时抛出了异常
修改此处传如的参数,改为文件的绝对路径
file.transferTo(filePath.getAbsoluteFile());
Postman测试截图
上传成功!
执行之后代码所在目录下面已经创建了一个imgFile目录
imgFile文件夹中已经有了上传的图片
原因分析
上面失败与成功只是因为路径所代表的是相对路径和绝对路径的区别。这就说明是MultiparFile的transferTo方法有问题了。让我们加一个断点,调试走一波!debug!
补充一个debug的小知识:
debug tips:
step into: 单步执行,遇到子函数就进入并且继续单步执行(F5)
step over: 在单步执行时,在函数内遇到子函数时不会进入子函数内单步执行,而是将子函数整个执行完再停止,也就是把子函数整个作为一步(F6)
step return: 在单步执行到子函数内时,用step return就可以执行完子函数余下部分,并返回上一层。
setp out: 效果同 step return。
我这里只给file.transferTo(filePath.getAbsoluteFile());这行代码加了断点,这里我给出调试中最重要的两个步骤:
调试中代码的执行流程是:
但代码进入 transferTo 后,然后执行 this.part.write(dest.getpath)方法,进入 write 方法内部,到这里就可以得到我们的答案了!
@Override public void transferTo(File dest) throws IOException, IllegalStateException { 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())); } } @Override public void write(String fileName) throws IOException { File file = new File(fileName); if (!file.isAbsolute()) { file = new File(location, fileName); } try { fileItem.write(file); } catch (Exception e) { throw new IOException(e); } }
这个write方法,会判断传入的参数是否是相对路径,如果是相对路径,它会自己给我们拼接一个父路径! 所以你应该知道那个奇怪的路径是哪里来的了吧!
C:\Users\Alfred\AppData\Local\Temp\tomcat.8080.2388870592947355119\work\Tomcat\localhost\ROOT\.\imgFile\684918a520684801b658c85a02bf9ba5.jpg
好了,大概可以理清了,这是因为transferTo的参数,如果是相对路径的话,程序会自己拼接一个父路径,因为我指定的相对路径中带有一个不存在的路径,如果尝试保存是会失败的。但是如果你传入的参数只是一个文件名,那应该就能保存成功。但是这样,取文件的时候,又会遇到问题了,你可能都不知道文件在哪里!
补充 一下吧
这里还有一个很有意思的地方,如果我的相对路径中不使用 . 开头,而只是以 / 开头,那么又会产生一个好玩的情况了。第一种情况就算刚才那样的,这里我们来讨论第二种情况,这种情况在Windows系统中还是同第一种一样的错误,但是在Linux系统中,它是可以正常执行的。如果你了解一点两个系统的知识的话,就应该知道Linux系统的根路径就是 /,所以以 / 开头的路径即是绝对路径。
所以这也算是程序跨平台需要考虑的问题了,如果不了解Linux的话,你可能不会明白,这里我给出一个验证程序实际测试一下。
Windows系统和Linux系统运行结果不同的代码。
import java.io.File; import java.io.IOException; public class OSMain { public static void main(String[] args) { String path1 = "./hehe"; String path2 = "/haha"; File file1 = new File(path1); File file2 = new File(path2); System.out.println("file1: " + file1 + " file1是绝对路径吗? " + file1.isAbsolute()); System.out.println("file2: " + file1 + " file2是绝对路径吗? " + file2.isAbsolute()); try { System.out.println(file1.getCanonicalPath()); System.out.println(file2.getCanonicalPath()); } catch (IOException e) { e.printStackTrace(); } } }
Windows运行结果
Linux运行结果
这里需要一个Linux环境,但是我的电脑上面没有,虽然我买了一台阿里云服务器。但是为了这么小小的一段代码登陆阿里云服务器去执行,我又嫌麻烦。还好我想到了一个更加巧妙的方法!
以前,知乎上面曾经有一个问题是关于菜鸟教程的,然后菜鸟教程的作者亲自出来回答了问题,并且贴了一张图片——菜鸟教程技术结构图谱
这个图片本身其实是涉及到了很多的,但是我们这里只关注一个就是在线代码提交执行,看到那只可爱的鲸鱼了吗?对,它就是docker。Docker里面就是一个完整的操作系统,并且是Linux系统!
好了,打开 菜鸟教程–>java教程–>随便找一个运行实例,进去删除原来的代码,复制我这个代码上去执行,输出结果!嘿嘿
注意:
有些在线代码执行是屏蔽了某些包的,所以有的也不一定是可以执行成功的,如果这里作者对在线代码提交执行做了那种限制,我们还是只能老老实实的去Linux系统上面执行了。
不过,有时候站在巨人的肩膀上,真的是挺轻松的!
以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。