浅谈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系统上面执行了。

不过,有时候站在巨人的肩膀上,真的是挺轻松的!

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • SpringMVC 文件上传配置,多文件上传,使用的MultipartFile的实例

    基本的SpringMVC的搭建在我的上一篇文章里已经写过了,这篇文章主要说明一下如何使用SpringMVC进行表单上的文件上传以及多个文件同时上传的步骤 文件上传项目的源码下载地址:demo 一.配置文件: SpringMVC 用的是 的MultipartFile来进行文件上传 所以我们首先要配置MultipartResolver:用于处理表单中的file <!-- 配置MultipartResolver 用于文件上传 使用spring的CommosMultipartResolver -->

  • MultipartFile中transferTo(File file)的路径问题及解决

    transferTo(File file)的路径问题 今天看到layui的文件上传的控件,就尝试了一下.简单创建了一个SpringMVC项目.记得在配置文件中注入以下Bean. <!-- 定义文件上传解析器 --> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!-- 设定

  • SpringMVC使用MultipartFile实现文件上传

    本文实例为大家分享了SpringMVC使用MultipartFile实现文件上传的具体代码,供大家参考,具体内容如下 一.配置文件 SpringMVC 用的是 的MultipartFile来进行文件上传 所以我们首先要配置MultipartResolver:用于处理表单中的file <!-- 配置MultipartResolver 用于文件上传 使用spring的CommosMultipartResolver --> <beans:bean id="multipartResol

  • Java利用MultipartFile实现上传多份文件的代码

    配置文件 <!-- 文件上传 --> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="defaultEncoding" value="utf-8"></property> <prop

  • SpringMVC使用MultipartFile 实现异步上传方法介绍

    目的是实现异步上传 1.添加pom依赖 添加pom依赖,因为用的ajax,数据需要转成json的格式进行传输,所以还有加入一个JSON jar包: <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.1</version> </dependency>

  • 浅谈MultipartFile中transferTo方法的坑

    前言:最近用SpringBoot写文件上传功能,使用参数绑定之后确实是非常的方便了. 但是,项目部署就出现了问题,搞得我一脸懵逼. 后来,才发现是因为我使用了相对路径导致的,这个绝对是一个坑人的地方,不过也说明需要学习的东西还有很多! 案例再现 @PostMapping("/uploadFile") public String uploadImg(@RequestParam("file") MultipartFile file, @RequestParam(&quo

  • 浅谈java中unmodifiableList方法的应用场景

    java对象中primitive类型变量可以通过不提供set方法保证不被修改,但对象的List成员在提供get方法后,就可以随意add.remove改变其结构,这不是希望的结果.网上看了下,发现Collections的静态方法unmodifiableList可以达到目的.方法原型为:public static <T> List<T> unmodifiableList(List<? extends T> list);用法也很简单,传入一个List实例la,返回这个list

  • 浅谈javascript中replace()方法

    定义和用法 replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串. 语法 stringObject.replace(regexp/substr,replacement) 返回值 一个新的字符串,是用 replacement 替换了 regexp 的第一次匹配或所有匹配之后得到的. 说明 字符串 stringObject 的 replace() 方法执行的是查找并替换的操作.它将在 stringObject 中查找与 regexp 相匹配的子字符串,然后

  • 浅谈jQuery中replace()方法

    今天在读jquery源码时,发现一个以前自己不曾注意过得问题,就是replece()的第二个参数为函数时的问题,以前只是知道replace()的第二个参数可以为函数,但是不知道该怎么操作,今天看到源码里用到了函数作为replace()的第二个参数时,感觉自己读起来比较吃力,于是准备整理下这个函数... 语法 stringObject.replace( regexp/substr, replacement) 返回值 返回一个新的字符串,是用replacement替换了regexp的第一次匹配或所欲

  • 浅谈Python中copy()方法的使用

    copy()方法返回字典的浅拷贝. 语法 以下是copy()方法的语法: dict.copy() 参数 NA 返回值 此方法返回字典的浅拷贝. 例子 下面的例子显示了copy()方法的使用. #!/usr/bin/python dict1 = {'Name': 'Zara', 'Age': 7}; dict2 = dict1.copy() print "New Dictinary : %s" % str(dict2) 当我们运行上面的程序,它会产生以下结果: New Dictinary

  • 浅谈jQuery中setInterval()方法

    定义和用法: setInterval() 方法可按照指定的周期(以毫秒计)来调用函数或计算表达式. setInterval() 方法会不停地调用函数,直到 clearInterval() 被调用或窗口被关闭.由 setInterval() 返回的 ID 值可用作 clearInterval() 方法的参数. var time=0: 用法1: function jump(){ ---- //函数内容 } time = setInterval("jump",5000); //每个五秒调用一

  • 浅谈C++中replace()方法

    本文主要针对c++中常用replace函数用法给出九个样例程序: 用法一: /* *用str替换指定字符串从起始位置pos开始长度为len的字符 *string& replace (size_t pos, size_t len, const string& str); */ int main() { string line = "this@ is@ a test string!"; line = line.replace(line.find("@")

  • 浅谈Django中的QueryDict元素为数组的坑

    一般在Django的视图函数中使用request.POST来获取请求体,request.POST是QueryDict类,通常作为dict来使用. 正常如下图 但是昨天在使用的时候遇到一个错误,提示从QueryDict里面pop出来的值类型为list. 一脸懵逼 在命令行敲代码,发现了这个坑, 如下图 可以看到,pop出来的值被放在的一个list里面.关键这个不同版本之间,行为还有所不同,就是因为在开发机器上一切正常,到了测试服务器就出问题才被发现的. 知道了问题,解决起来也简单,直接调用Quer

  • 浅谈Java list.remove( )方法需要注意的两个坑

    list.remove 最近做项目的过程中,需要用到list.remove()方法,结果发现两个有趣的坑,经过分析后找到原因,记录一下跟大家分享一下. 代码 直接上一段代码,进行分析. public class Main { public static void main(String[] args) { List<String> stringList = new ArrayList<>();//数据集合 List<Integer> integerList = new

  • 浅谈Python3中datetime不同时区转换介绍与踩坑

    最近的项目需要根据用户所属时区制定一些特定策略,学习.应用了若干python3的时区转换相关知识,这里整理一部分记录下来. 下面涉及的几个概念及知识点: GMT时间:Greenwich Mean Time, 格林尼治平均时间 UTC时间:Universal Time Coordinated 世界协调时,可以认为是更精准的GMT时间,但两者误差极小,在1s以内,一般可视为等同 LMT:Local Mean Time, 当地标准时间 Python中的北京时间:Python的标准timezone中信息

随机推荐