基于SpringBoot實(shí)現(xiàn)文件秒傳功能
前言
在開發(fā)Web應(yīng)用時(shí),文件上傳是一個(gè)常見需求。然而,當(dāng)用戶需要上傳大文件或相同文件多次時(shí),會(huì)造成帶寬浪費(fèi)和服務(wù)器存儲(chǔ)冗余。此時(shí)可以使用文件秒傳技術(shù)通過識(shí)別重復(fù)文件,實(shí)現(xiàn)瞬間完成上傳的效果,大大提升了用戶體驗(yàn)和系統(tǒng)效率。
文件秒傳原理
文件秒傳的核心原理是:
- 計(jì)算文件唯一標(biāo)識(shí)(通常是MD5或SHA256值)
- 上傳前先檢查服務(wù)器是否已存在相同標(biāo)識(shí)的文件
- 若存在,則直接引用已有文件,無(wú)需再次上傳
- 若不存在,則執(zhí)行常規(guī)上傳流程
這種方式能顯著減少網(wǎng)絡(luò)傳輸和避免存儲(chǔ)冗余。
代碼實(shí)現(xiàn)
1. 創(chuàng)建項(xiàng)目基礎(chǔ)結(jié)構(gòu)
首先創(chuàng)建Spring Boot項(xiàng)目,添加必要依賴:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.18</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.1</version>
</dependency>
</dependencies>
2. 創(chuàng)建上傳存儲(chǔ)代碼
此處使用一個(gè)簡(jiǎn)單的集合來(lái)存儲(chǔ)文件信息,實(shí)際使用需要替換為數(shù)據(jù)庫(kù)或其他持久化中間件。
import cn.hutool.crypto.digest.DigestUtil;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class FileService {
// 使用Map存儲(chǔ)文件信息,key為MD5,value為文件信息(實(shí)際使用時(shí)可替換為數(shù)據(jù)庫(kù)存儲(chǔ))
private final Map<String, FileInfo> fileStore = new ConcurrentHashMap<>();
/**
* 檢查文件是否已存在
*/
public FileInfo findByMd5(String md5) {
return fileStore.get(md5);
}
/**
* 保存文件信息
*/
public FileInfo saveFile(String fileName, String fileMd5, Long fileSize, String filePath) {
FileInfo fileInfo = new FileInfo(fileName, fileMd5, fileSize, filePath);
fileStore.put(fileMd5, fileInfo); // 實(shí)際使用時(shí)插入數(shù)據(jù)庫(kù)
return fileInfo;
}
/**
* 計(jì)算文件MD5
*/
public String calculateMD5(MultipartFile file) throws IOException {
return DigestUtil.md5Hex(file.getInputStream());
}
}
定義一個(gè)簡(jiǎn)單的文件信息實(shí)體類:
import cn.hutool.core.util.IdUtil;
public class FileInfo {
private String id = IdUtil.fastUUID();
private String fileName;
private String fileMd5;
private Long fileSize;
private String filePath;
public FileInfo(String fileName, String fileMd5, Long fileSize, String filePath) {
this.fileName = fileName;
this.fileMd5 = fileMd5;
this.fileSize = fileSize;
this.filePath = filePath;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getFileMd5() {
return fileMd5;
}
public void setFileMd5(String fileMd5) {
this.fileMd5 = fileMd5;
}
public Long getFileSize() {
return fileSize;
}
public void setFileSize(Long fileSize) {
this.fileSize = fileSize;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
}
3. 創(chuàng)建Result類
為了統(tǒng)一返回結(jié)果格式,可以創(chuàng)建一個(gè)簡(jiǎn)單的Result類。
public class Result {
private boolean success;
private Object data;
private String message;
public Result(boolean success, Object data, String message) {
this.success = success;
this.data = data;
this.message = message;
}
public static Result success(Object data) {
return new Result(true, data,"success");
}
public static Result success(Object data,String message) {
return new Result(true, data,message);
}
public static Result error(String message) {
return new Result(false, null, message);
}
// Getters
public boolean isSuccess() { return success; }
public Object getData() { return data; }
public String getMessage() { return message; }
}
4. 創(chuàng)建Controller控制器
import cn.hutool.core.io.FileUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
@RestController
@RequestMapping("/api/file")
public class FileController {
private static Logger logger = LoggerFactory.getLogger(FileController.class);
@Autowired
private FileService fileService;
/**
* 檢查文件是否已存在
*/
@PostMapping("/check")
public Result checkFile(@RequestParam("md5") String md5) {
FileInfo fileInfo = fileService.findByMd5(md5);
if (fileInfo != null) {
return Result.success(fileInfo);
}
return Result.success(null);
}
/**
* 上傳文件
*/
@PostMapping("/upload")
public Result uploadFile(@RequestParam("file") MultipartFile file) {
try {
// 計(jì)算文件MD5值
String md5 = fileService.calculateMD5(file);
// 檢查文件是否已存在
FileInfo existFile = fileService.findByMd5(md5);
if (existFile != null) {
// todo 進(jìn)行自定義的邏輯處理
return Result.success(existFile,"文件秒傳成功");
}
// 文件不存在,執(zhí)行上傳
String originalFilename = file.getOriginalFilename();
String filePath = FileUtil.getTmpDir() + File.separator + originalFilename; // 保存到臨時(shí)目錄
// 存儲(chǔ)文件
file.transferTo(new File(filePath));
// 保存文件信息到內(nèi)存(實(shí)際使用時(shí)應(yīng)替換為數(shù)據(jù)庫(kù))
FileInfo fileInfo = fileService.saveFile(originalFilename, md5, file.getSize(), filePath);
return Result.success(fileInfo,"文件上傳成功");
} catch (Exception e) {
logger.error(e.getMessage(),e);
return Result.error("文件上傳失?。? + e.getMessage());
}
}
}
4. 創(chuàng)建純HTML前端頁(yè)面
創(chuàng)建一個(gè)簡(jiǎn)單的HTML上傳頁(yè)面:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>文件秒傳示例</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
</head>
<body>
<h2>文件上傳(支持秒傳)</h2>
<input type="file" id="fileInput" />
<button onclick="uploadFile()">上傳文件</button>
<div id="progressBar" style="display:none;">
<div>上傳進(jìn)度:<span id="progress">0%</span></div>
</div>
<div id="result"></div>
<script>
function uploadFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('請(qǐng)選擇文件');
return;
}
document.getElementById('progressBar').style.display = 'block';
document.getElementById('result').innerText = '計(jì)算文件MD5中...';
// 計(jì)算文件MD5
calculateMD5(file).then(md5 => {
document.getElementById('result').innerText = '正在檢查文件是否已存在...';
// 檢查文件是否已存在
return axios.post('/api/file/check', {
md5: md5
}).then(response => {
if (response.data.data && response.data.data.id) {
// 文件已存在,執(zhí)行秒傳
document.getElementById('result').innerText = '文件秒傳成功!';
document.getElementById('progress').innerText = '100%';
return Promise.resolve();
} else {
// 文件不存在,執(zhí)行上傳
const formData = new FormData();
formData.append('file', file);
return axios.post('/api/file/upload', formData, {
onUploadProgress: progressEvent => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
document.getElementById('progress').innerText = percentCompleted + '%';
}
}).then(response => {
document.getElementById('result').innerText = '文件上傳成功!';
});
}
});
}).catch(error => {
document.getElementById('result').innerText = '錯(cuò)誤:' + error.message;
});
}
// 計(jì)算文件MD5
function calculateMD5(file) {
return new Promise((resolve, reject) => {
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
const chunkSize = 2097152; // 2MB
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
fileReader.onload = function(e) {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
resolve(spark.end());
}
};
fileReader.onerror = function() {
reject('文件讀取錯(cuò)誤');
};
function loadNext() {
const start = currentChunk * chunkSize;
const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
});
}
</script>
</body>
</html>
5. 配置文件
在application.yml中添加必要配置
server:
port: 8080
spring:
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
效果
第一次上傳

第二次上傳

到此這篇關(guān)于基于SpringBoot實(shí)現(xiàn)文件秒傳功能的文章就介紹到這了,更多相關(guān)SpringBoot文件秒傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java Map 通過 key 或者 value 過濾的實(shí)例代碼
這篇文章主要介紹了Java Map 通過 key 或者 value 過濾的實(shí)例代碼,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-06-06
Java語(yǔ)法基礎(chǔ)之函數(shù)的使用說(shuō)明
函數(shù)就是定義在類中的具有特定功能的一段小程序,函數(shù)也稱為方法2013-07-07
Java實(shí)現(xiàn)迷你圖書管理系統(tǒng)案例全程
這篇文章主要為大家詳細(xì)介紹了如何利用java語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單的圖書管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-12-12
Java編程中利用InetAddress類確定特殊IP地址的方法
這篇文章主要介紹了Java編程中利用InetAddress類確定特殊IP地址的方法,InetAddress類是Java網(wǎng)絡(luò)編程中一個(gè)相當(dāng)實(shí)用的類,需要的朋友可以參考下2015-11-11
Spring事務(wù)失效的一種原因關(guān)于this調(diào)用的問題
這篇文章主要介紹了Spring事務(wù)失效的一種原因關(guān)于this調(diào)用的問題,本文給大家分享問題原因及解決辦法,通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-10-10
SpringBoot帶你實(shí)現(xiàn)一個(gè)點(diǎn)餐小程序
有個(gè)小伙伴臨時(shí)找到我,要開發(fā)一個(gè)點(diǎn)餐的系統(tǒng),時(shí)間比較著急,給了2天的時(shí)間。馬馬虎虎的搞出來(lái)了,頭發(fā)掉了一撮!下面介紹下本系統(tǒng),感興趣的小伙伴,可以參考開發(fā)下2022-07-07

