淺談MultipartFile中transferTo方法的坑
前言:最近用SpringBoot寫文件上傳功能,使用參數(shù)綁定之后確實是非常的方便了。
但是,項目部署就出現(xiàn)了問題,搞得我一臉懵逼。
后來,才發(fā)現(xiàn)是因為我使用了相對路徑導(dǎo)致的,這個絕對是一個坑人的地方,不過也說明需要學(xué)習(xí)的東西還有很多!
案例再現(xiàn)
@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);
// 第一次執(zhí)行代碼時,路徑是不存在的
logger.info("文件保存路徑:{},是否存在:{}", filePath.getParentFile().exists(), filePath.getParent());
if (!filePath.getParentFile().exists()) { // 如果存放路徑的父目錄不存在,就創(chuàng)建它。
filePath.getParentFile().mkdirs();
}
// 如果路徑不存在,上面的代碼會創(chuàng)建路徑,此時路徑即已經(jīng)創(chuàng)建好了
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 "上傳失敗";
}
我在日志中打印了路徑的位置,顯示是沒有問題,當(dāng)時一旦執(zhí)行到file.transferTo(filePath);就會產(chǎn)生一個FileNotFoundException,但是我前面的代碼是執(zhí)行了,并且創(chuàng)建了一個文件夾的。
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 (系統(tǒng)找不到指定的路徑。)
注意: 這里雖然沒有什么頭緒,當(dāng)時觀察日志可以發(fā)現(xiàn),程序試圖將文件保存到一個很奇怪的目錄下,當(dāng)是這個目錄和前面那個filePath已經(jīng)沒有關(guān)系了,這里是一個疑點!
執(zhí)行之后代碼所在目錄下面已經(jīng)創(chuàng)建了一個imgFile目錄

imgFile文件夾中是空的,因為執(zhí)行transferTo時拋出了異常

修改此處傳如的參數(shù),改為文件的絕對路徑
file.transferTo(filePath.getAbsoluteFile());
Postman測試截圖
上傳成功!

執(zhí)行之后代碼所在目錄下面已經(jīng)創(chuàng)建了一個imgFile目錄

imgFile文件夾中已經(jīng)有了上傳的圖片

原因分析
上面失敗與成功只是因為路徑所代表的是相對路徑和絕對路徑的區(qū)別。這就說明是MultiparFile的transferTo方法有問題了。讓我們加一個斷點,調(diào)試走一波!debug!
補充一個debug的小知識:
debug tips:
step into: 單步執(zhí)行,遇到子函數(shù)就進入并且繼續(xù)單步執(zhí)行(F5)
step over: 在單步執(zhí)行時,在函數(shù)內(nèi)遇到子函數(shù)時不會進入子函數(shù)內(nèi)單步執(zhí)行,而是將子函數(shù)整個執(zhí)行完再停止,也就是把子函數(shù)整個作為一步(F6)
step return: 在單步執(zhí)行到子函數(shù)內(nèi)時,用step return就可以執(zhí)行完子函數(shù)余下部分,并返回上一層。
setp out: 效果同 step return。
我這里只給file.transferTo(filePath.getAbsoluteFile());這行代碼加了斷點,這里我給出調(diào)試中最重要的兩個步驟:
調(diào)試中代碼的執(zhí)行流程是:
但代碼進入 transferTo 后,然后執(zhí)行 this.part.write(dest.getpath)方法,進入 write 方法內(nèi)部,到這里就可以得到我們的答案了!
@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方法,會判斷傳入的參數(shù)是否是相對路徑,如果是相對路徑,它會自己給我們拼接一個父路徑! 所以你應(yīng)該知道那個奇怪的路徑是哪里來的了吧!
C:\Users\Alfred\AppData\Local\Temp\tomcat.8080.2388870592947355119\work\Tomcat\localhost\ROOT\.\imgFile\684918a520684801b658c85a02bf9ba5.jpg
好了,大概可以理清了,這是因為transferTo的參數(shù),如果是相對路徑的話,程序會自己拼接一個父路徑,因為我指定的相對路徑中帶有一個不存在的路徑,如果嘗試保存是會失敗的。但是如果你傳入的參數(shù)只是一個文件名,那應(yīng)該就能保存成功。但是這樣,取文件的時候,又會遇到問題了,你可能都不知道文件在哪里!
補充 一下吧
這里還有一個很有意思的地方,如果我的相對路徑中不使用 . 開頭,而只是以 / 開頭,那么又會產(chǎn)生一個好玩的情況了。第一種情況就算剛才那樣的,這里我們來討論第二種情況,這種情況在Windows系統(tǒng)中還是同第一種一樣的錯誤,但是在Linux系統(tǒng)中,它是可以正常執(zhí)行的。如果你了解一點兩個系統(tǒng)的知識的話,就應(yīng)該知道Linux系統(tǒng)的根路徑就是 /,所以以 / 開頭的路徑即是絕對路徑。
所以這也算是程序跨平臺需要考慮的問題了,如果不了解Linux的話,你可能不會明白,這里我給出一個驗證程序?qū)嶋H測試一下。
Windows系統(tǒng)和Linux系統(tǒng)運行結(jié)果不同的代碼。
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運行結(jié)果

Linux運行結(jié)果
這里需要一個Linux環(huán)境,但是我的電腦上面沒有,雖然我買了一臺阿里云服務(wù)器。但是為了這么小小的一段代碼登陸阿里云服務(wù)器去執(zhí)行,我又嫌麻煩。還好我想到了一個更加巧妙的方法!
以前,知乎上面曾經(jīng)有一個問題是關(guān)于菜鳥教程的,然后菜鳥教程的作者親自出來回答了問題,并且貼了一張圖片——菜鳥教程技術(shù)結(jié)構(gòu)圖譜

這個圖片本身其實是涉及到了很多的,但是我們這里只關(guān)注一個就是在線代碼提交執(zhí)行,看到那只可愛的鯨魚了嗎?對,它就是docker。Docker里面就是一個完整的操作系統(tǒng),并且是Linux系統(tǒng)!
好了,打開 菜鳥教程–>java教程–>隨便找一個運行實例,進去刪除原來的代碼,復(fù)制我這個代碼上去執(zhí)行,輸出結(jié)果!嘿嘿

注意:
有些在線代碼執(zhí)行是屏蔽了某些包的,所以有的也不一定是可以執(zhí)行成功的,如果這里作者對在線代碼提交執(zhí)行做了那種限制,我們還是只能老老實實的去Linux系統(tǒng)上面執(zhí)行了。
不過,有時候站在巨人的肩膀上,真的是挺輕松的!
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Springboot實現(xiàn)Java郵件任務(wù)過程解析
這篇文章主要介紹了Springboot實現(xiàn)Java郵件任務(wù)過程解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-09-09
SpringBoot集成ElasticSearch(ES)實現(xiàn)全文搜索功能
Elasticsearch是一個開源的分布式搜索和分析引擎,它被設(shè)計用于處理大規(guī)模數(shù)據(jù)集,它提供了一個分布式多用戶能力的全文搜索引擎,本文將給大家介紹SpringBoot集成ElasticSearch(ES)實現(xiàn)全文搜索功能,需要的朋友可以參考下2024-02-02
Java concurrency集合之ConcurrentSkipListMap_動力節(jié)點Java學(xué)院整理
這篇文章主要為大家詳細介紹了Java concurrency集合之ConcurrentSkipListMap的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06
詳解使用Spring Boot開發(fā)Restful程序
本篇文章主要介紹了詳解使用Spring Boot開發(fā)Restful程序,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05
springboot?vue測試平臺接口定義前后端新增功能實現(xiàn)
這篇文章主要介紹了springboot?vue測試平臺接口定義前后端新增功能實現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-05-05
Java線程池ThreadPoolExecutor的使用及其原理詳細解讀
這篇文章主要介紹了Java線程池ThreadPoolExecutor的使用及其原理詳細解讀,線程池是一種多線程處理形式,處理過程中將任務(wù)添加到隊列,然后在創(chuàng)建線程后自動啟動這些任務(wù),線程池線程都是后臺線程,需要的朋友可以參考下2023-12-12

