需求背景:Java後端項目上傳檔案是一個很常見的需求,一般正式項目中我們上傳檔案都是利用第三方阿裡雲OSS這類的,但是如果隻是為了學習之用,那我們可能就會直接上傳到電腦上某個本地檔案夾。
但是上傳到自己電腦上某個檔案夾,那換一台電腦就看不到了,還有一般檔案上傳之後我們還需要傳回給前端檔案的下載下傳路徑,如果是電腦上随便某個檔案夾,那前端很可能是通路不到的。
為了解決前端通路這個問題,我們可以把檔案上傳到後端服務的靜态資源目錄裡,這樣前端就可以直接通過後端服務的位址和端口加上資源路徑來通路了。
實作思路
上傳檔案的路徑我們可以用 ResourceUtils.getURL("classpath:").getPath() 這個方法來擷取,拿到的就是編譯後的 target/classes 目錄的絕對路徑,前端上傳的檔案就可以直接存到這個下面的目錄,比如:target/classes/upload/logo.jpg,給前端傳回的下載下傳位址就像這樣的:http://localhost:8080/upload/logo.jpg。
上面的思路确實解決了上傳和下載下傳的問題,但是 target 目錄是會變動的,而且不會送出到代碼倉庫,如果我們清理後再重新編譯或者換台電腦編譯,之前上傳的檔案就都沒了。
這可怎麼辦呢?仔細一想我們項目不是有一個叫 resources 用來存放靜态資源的目錄嗎,這個目錄正常也會送出到代碼倉庫進行管理的,那我們每次上傳的檔案不就可以一塊送出到倉庫裡,這部就實作了永久儲存。
說幹就幹,就直接将檔案儲存到 resources/upload 目錄下,後端一run前端一上傳,檔案确實被儲存到了 resources/upload 目錄下。再仔細一看不對,前端的位址沒發通路剛上傳的檔案,因為 target/classes 目錄下壓根沒有剛上傳的檔案,重新點一次 compile 編譯後将 resources 目錄下的檔案同步到了 target/classes 目錄下确實可以實作通路,但是總不能我們每次上傳後都要自己重新點一下編譯重新運作吧。
最後一合計,那我把resources和target結合一下,将檔案同時儲存到這兩個目錄下,是不是就可以實作永久儲存和實時通路了呢。
終極方案
用System.getProperty("user.dir")可以擷取到項目的工作目錄,再拼上項目的結構目錄就可以拿到 resources 目錄的絕對路徑;target/classes 運作目錄可以用 ResourceUtils.getURL("classpath:").getPath() 擷取。
注意如果最後上傳的資源目錄通路404,要看下 application.yml 裡 spring.mvn 的靜态資源路徑,pom.xml裡的 resources過濾規則,還有 WebMvcConfiguration 裡配置的 addResourceHandler 靜态資源攔映射有沒有攔截掉。
最後前端傳過來的是一個 File 檔案,但是一個檔案其實是沒辦法循環去儲存到多個目錄下的,第一個檔案夾儲存成功後後面的都會報錯,想一下我們平時在電腦上儲存一個檔案也隻能儲存到一個目錄下,再要儲存到其他目錄則自己複制一份過去就好了,這裡也是一樣第一個目錄我們直接儲存,第二個則可以用 spring 提供的 FileCopyUtils.copy 直接複制檔案就可以了。
完整代碼
UploadFileUtil.java
package com.sky.utils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.web.multipart.MultipartFile;
public class UploadFileUtil {
/**
* 擷取檔案儲存路徑
*
* @return File
* @throws FileNotFoundException
*/
static List<File> getUploadDirectory() throws FileNotFoundException {
// 開發環境擷取 target/classes 目錄:清理重新編譯後就沒有了
File targetPath = new File(ResourceUtils.getURL("classpath:").getPath());
// System.out.printf("項目運作的絕對路徑:" + path.getAbsolutePath());
// 輸出 xx/sky-parent/sky-server/target/classes
// 生産環境 不存在 target/classes 目錄
if (!targetPath.exists()) {
// 擷取目前運作目錄
targetPath = new File("");
}
// 開發環境 resources 目錄:可永久儲存
String resourcesPath = System.getProperty("user.dir") + "/sky-server/src/main/resources";
// System.out.printf("resources目錄路徑:" + resourcesPath);
File path = new File(resourcesPath);
File upload = new File(path.getAbsolutePath(), "upload");
File uploadTarget = new File(targetPath.getAbsolutePath(), "upload");
// 不存在則建立
if (!upload.exists()) {
upload.mkdirs();
}
if (!uploadTarget.exists()) {
uploadTarget.mkdirs();
}
List<File> files = new ArrayList<File>();
files.add(upload);
files.add(uploadTarget);
// System.out.printf("目前目錄:" + files);
return files;
}
public static String upload(MultipartFile myFile, String dir) throws IOException {
String filePath = "";
if (!myFile.isEmpty()) {
try {
String filename = myFile.getOriginalFilename();
filename = UUID.randomUUID() + filename.substring(filename.lastIndexOf("."));
// 之是以儲存到 resources 和 target 兩個目錄,兼顧開發測試和永久儲存
// 隻儲存到resources目錄下每次上傳了要重新編譯下,target則清理打包後就沒有了
List<File> files = getUploadDirectory();
// 注意這裡一個檔案不能循環同時寫入多個目錄,儲存了第一個,第二個要複制過去
File curFile = new File(files.get(0), filename);
myFile.transferTo(curFile);
FileCopyUtils.copy(curFile, new File(files.get(1), filename));
//for (File f: files) {
//File curFile = new File(f, filename);
//myFile.transferTo(curFile);
//}
filePath = "http://localhost:8080/upload/" + filename;
} catch (Exception e) {
e.printStackTrace();
}
}
return filePath;
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>upload/**</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
application.yml
server:
port: 8080
spring:
mvc:
static-path-pattern: /upload/**
WebMvcConfiguration
package com.sky.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.util.List;
/**
* 配置類,注冊web層相關元件
*/
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
/**
* 設定靜态資源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
// 設定上傳的檔案靜态資源映射,application 裡的 mvc 裡也要設定下靜态目錄
registry.addResourceHandler("/upload/**")
.addResourceLocations("classpath:/upload/", "file:upload/");
}
}
使用示例
在 controller 接收前端用表單上傳的 File 檔案
package com.sky.controller.common;
import com.sky.result.Result;
import com.sky.utils.UploadFileUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
/**
* 公共請求
*/
@RestController
@RequestMapping("/common")
@Api(tags = "公共")
@Slf4j
public class CommonController {
@PostMapping("/upload")
@ApiOperation("上傳檔案")
public Result uploadFile(MultipartFile file) throws IOException {
log.info("上傳檔案:{}", file);
String fileUrl = UploadFileUtil.upload(file, "");
if (fileUrl == null || fileUrl == "") {
return Result.error("上傳失敗");
}
return Result.success(fileUrl);
}
}