SpringBoot 實戰:文件上傳之秒傳、斷點續傳、分片上傳
文件上傳功能幾乎是每個 Web 應用不可或缺的一部分。無論是個人博客中的圖片上傳,還是企業級應用中的文檔管理,文件上傳都扮演著至關重要的角色。今天,松哥和大家來聊聊文件上傳中的幾個高級玩法——秒傳、斷點續傳和分片上傳。
一 文件上傳的常見場景
在日常開發中,文件上傳的場景多種多樣。比如,在線教育平臺上的視頻資源上傳,社交平臺上的圖片分享,以及企業內部的知識文檔管理等。這些場景對文件上傳的要求也各不相同,有的追求速度,有的注重穩定性,還有的需要考慮文件大小和安全性。因此,針對不同需求,我們有了秒傳、斷點續傳和分片上傳等解決方案。
二 秒傳、斷點上傳與分片上傳
秒傳
秒傳,顧名思義,就是幾乎瞬間完成文件上傳的過程。其實現原理是通過計算文件的哈希值(如 MD5 或 SHA-1),然后將這個唯一的標識符發送給服務器。如果服務器上已經存在相同的文件,則直接返回成功信息,避免了重復上傳。這種方式不僅節省了帶寬,也大大提高了用戶體驗。
斷點續傳
斷點續傳是指在網絡不穩定或者用戶主動中斷上傳后,能夠從上次中斷的地方繼續上傳,而不需要重新開始整個過程。這對于大文件上傳尤為重要,因為它可以有效防止因網絡問題導致的上傳失敗,同時也能節約用戶的流量和時間。
分片上傳
分片上傳則是將一個大文件分割成多個小塊分別上傳,最后再由服務器合并成完整的文件。這種做法的好處是可以并行處理多個小文件,提高上傳效率;同時,如果某一部分上傳失敗,只需要重傳這一部分,不影響其他部分。
三 秒傳實戰
后端實現
在 SpringBoot 項目中,我們可以使用 MessageDigest 類來計算文件的 MD5 值,然后檢查數據庫中是否存在該文件。
@RestController
@RequestMapping("/file")
public class FileController {
@Autowired
FileService fileService;
@PostMapping("/upload1")
public ResponseEntity<String> secondUpload(@RequestParam(value = "file",required = false) MultipartFile file,@RequestParam(required = false,value = "md5") String md5) {
try {
// 檢查數據庫中是否已存在該文件
if (fileService.existsByMd5(md5)) {
return ResponseEntity.ok("文件已存在");
}
// 保存文件到服務器
file.transferTo(new File("/path/to/save/" + file.getOriginalFilename()));
// 保存文件信息到數據庫
fileService.save(new FileInfo(file.getOriginalFilename(), DigestUtils.md5DigestAsHex(file.getInputStream())));
return ResponseEntity.ok("上傳成功");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("上傳失敗");
}
}
}
前端調用
前端可以通過 JavaScript 的 FileReader API 讀取文件內容,通過 spark-md5 計算 MD5 值,然后發送給后端進行校驗。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>秒傳</title>
<script src="spark-md5.js"></script>
</head>
<body>
<input type="file" id="fileInput" />
<button onclick="startUpload()">開始上傳</button>
<hr>
<script>
async function startUpload() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert("請選擇文件");
return;
}
const md5 = await calculateMd5(file);
const formData = new FormData();
formData.append('md5', md5);
const response = await fetch('/file/upload1', {
method: 'POST',
body: formData
});
const result = await response.text();
if (response.ok) {
if (result != "文件已存在") {
// 開始上傳文件
}
} else {
console.error("上傳失敗: " + result);
}
}
function calculateMd5(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const spark = new SparkMD5.ArrayBuffer();
spark.append(reader.result);
resolve(spark.end());
};
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
});
}
</script>
</body>
</html>
前端分為兩個步驟:
- 計算文件的 MD5 值,計算之后發送給服務端確定文件是否存在。
- 如果文件已經存在,則不需要繼續上傳文件;如果文件不存在,則開始上傳文件,上傳文件和 MD5 校驗請求類似,上面的案例代碼中我就沒有重復演示了,松哥在書里和之前的課程里都多次講過文件上傳,這里不再啰嗦。
四 分片上傳實戰
分片上傳關鍵是在前端對文件切片,比如一個 10MB 的文件切為 10 份,每份 1MB。每次上傳的時候,需要多一個參數記錄當前上傳的文件切片的起始位置。
比如一個 10MB 的文件,切為 10 份,每份 1MB,那么:
- 第 0 片,從 0 開始,一共是 1024*1024 個字節。
- 第 1 片,從 1024*1024 開始,一共是 1024*1024 個字節。
- 第 2 片...
把這個搞懂,后面的代碼就好理解了。
后端實現
private static final String UPLOAD_DIR = System.getProperty("user.home") + "/uploads/";
/**
* 上傳文件到指定位置
*
* @param file 上傳的文件
* @param start 文件開始上傳的位置
* @return ResponseEntity<String> 上傳結果
*/
@PostMapping("/upload2")
public ResponseEntity<String> resumeUpload(@RequestParam("file") MultipartFile file, @RequestParam("start") long start,@RequestParam("fileName") String fileName) {
try {
File directory = new File(UPLOAD_DIR);
if (!directory.exists()) {
directory.mkdirs();
}
File targetFile = new File(UPLOAD_DIR + fileName);
RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "rw");
FileChannel channel = randomAccessFile.getChannel();
channel.position(start);
channel.transferFrom(file.getResource().readableChannel(), start, file.getSize());
channel.close();
randomAccessFile.close();
return ResponseEntity.ok("上傳成功");
} catch (Exception e) {
System.out.println("上傳失敗: "+e.getMessage());
return ResponseEntity.status(500).body("上傳失敗");
}
}
后端每次處理的時候,需要先設置文件的起始位置。
前端調用
前端需要將文件切分成多個小塊,然后依次上傳。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>分片示例</title>
</head>
<body>
<input type="file" id="fileInput" />
<button onclick="startUpload()">開始上傳</button>
<script>
async function startUpload() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert("請選擇文件");
return;
}
const filename = file.name;
let start = 0;
uploadFile(file, start);
}
async function uploadFile(file, start) {
const chunkSize = 1024 * 1024; // 每個分片1MB
const total = Math.ceil(file.size / chunkSize);
for (let i = 0; i < total; i++) {
const chunkStart = start + i * chunkSize;
const chunkEnd = Math.min(chunkStart + chunkSize, file.size);
const chunk = file.slice(chunkStart, chunkEnd);
const formData = new FormData();
formData.append('file', chunk);
formData.append('start', chunkStart);
formData.append('fileName', file.name);
const response = await fetch('/file/upload2', {
method: 'POST',
body: formData
});
const result = await response.text();
if (response.ok) {
console.log(`分片 ${i + 1}/${total} 上傳成功`);
} else {
console.error(`分片 ${i + 1}/${total} 上傳失敗: ${result}`);
break;
}
}
}
</script>
</body>
</html>
五 斷點續傳實戰
斷點續傳的技術原理類似于分片上傳。
當文件已經上傳了一部分之后,斷了需要重新開始上傳。
那么我們的思路是這樣的:
- 前端先發送一個請求,檢查要上傳的文件在服務端是否已經存在,如果存在,目前大小是多少。
- 前端根據已經存在的大小,繼續上傳文件即可。
后端案例
先來看后端檢查的接口,如下:
@GetMapping("/check")
public ResponseEntity<Long> checkFile(@RequestParam("filename") String filename) {
File file = new File(UPLOAD_DIR + filename);
if (file.exists()) {
return ResponseEntity.ok(file.length());
} else {
return ResponseEntity.ok(0L);
}
}
如果文件存在,則返回已經存在的文件大小。
如果文件不存在,則返回 0,表示前端從頭開始上傳該文件。
前端調用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>斷點續傳示例</title>
</head>
<body>
<input type="file" id="fileInput"/>
<button onclick="startUpload()">開始上傳</button>
<script>
async function startUpload() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert("請選擇文件");
return;
}
const filename = file.name;
let start = await checkFile(filename);
uploadFile(file, start);
}
async function checkFile(filename) {
const response = await fetch(`/file/check?filename=${filename}`);
const start = await response.json();
return start;
}
async function uploadFile(file, start) {
const chunkSize = 1024 * 1024; // 每個分片1MB
const total = Math.ceil((file.size - start) / chunkSize);
for (let i = 0; i < total; i++) {
const chunkStart = start + i * chunkSize;
const chunkEnd = Math.min(chunkStart + chunkSize, file.size);
const chunk = file.slice(chunkStart, chunkEnd);
const formData = new FormData();
formData.append('file', chunk);
formData.append('start', chunkStart);
formData.append('fileName', file.name);
const response = await fetch('/file/upload2', {
method: 'POST',
body: formData
});
const result = await response.text();
if (response.ok) {
console.log(`分片 ${i + 1}/${total} 上傳成功`);
} else {
console.error(`分片 ${i + 1}/${total} 上傳失敗: ${result}`);
break;
}
}
}
</script>
</body>
</html>
這個案例實際上是一個斷點續傳+分片上傳的案例,相關知識點并不難,小伙伴們可以自行體會下。
六 總結
好了,以上就是關于文件上傳中秒傳、斷點續傳和分片上傳的實戰分享。通過這些技術的應用,我們可以極大地提升文件上傳的效率和穩定性,改善用戶體驗。希望各位小伙伴在自己的項目中也能靈活運用這些技巧,解決實際問題。