SpringBoot實戰:檔案上傳之秒傳、斷點續傳、分片上傳

👉 這是一個或許對你有用的社群
🐱 一對一交流/面試小冊/簡歷最佳化/求職解惑,歡迎加入芋道快速開發平臺知識星球。下面是星球提供的部分資料:
👉這是一個或許對你有用的開源專案
國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。
功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、ERPCRMAI 大模型等等功能:
  • Boot 多模組架構:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服務架構:https://gitee.com/zhijiantianya/yudao-cloud
  • 影片教程:https://doc.iocoder.cn
【國內首批】支援 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 雙版本 

檔案上傳功能幾乎是每個 Web 應用不可或缺的一部分。無論是個人部落格中的圖片上傳,還是企業級應用中的文件管理,檔案上傳都扮演著至關重要的角色。今天,松哥和大家來聊聊檔案上傳中的幾個高階玩法——秒傳、斷點續傳和分片上傳。

一 檔案上傳的常見場景

在日常開發中,檔案上傳的場景多種多樣。比如,線上教育平臺上的影片資源上傳,社交平臺上的圖片分享,以及企業內部的知識文件管理等。這些場景對檔案上傳的要求也各不相同,有的追求速度,有的注重穩定性,還有的需要考慮檔案大小和安全性。因此,針對不同需求,我們有了秒傳、斷點續傳和分片上傳等解決方案。
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 影片教程:https://doc.iocoder.cn/video/

二 秒傳、斷點上傳與分片上傳

秒傳

秒傳,顧名思義,就是幾乎瞬間完成檔案上傳的過程。其實現原理是透過計算檔案的雜湊值(如 MD5 或 SHA-1),然後將這個唯一的識別符號傳送給伺服器。如果伺服器上已經存在相同的檔案,則直接返回成功資訊,避免了重複上傳。這種方式不僅節省了頻寬,也大大提高了使用者體驗。

斷點續傳

斷點續傳是指在網路不穩定或者使用者主動中斷上傳後,能夠從上次中斷的地方繼續上傳,而不需要重新開始整個過程。這對於大檔案上傳尤為重要,因為它可以有效防止因網路問題導致的上傳失敗,同時也能節約使用者的流量和時間。

分片上傳

分片上傳則是將一個大檔案分割成多個小塊分別上傳,最後再由伺服器合併成完整的檔案。這種做法的好處是可以並行處理多個小檔案,提高上傳效率;同時,如果某一部分上傳失敗,只需要重傳這一部分,不影響其他部分。
基於 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/yudao-cloud
  • 影片教程:https://doc.iocoder.cn/video/

三 秒傳實戰

後端實現

在 SpringBoot 專案中,我們可以使用 MessageDigest 類來計算檔案的 MD5 值,然後檢查資料庫中是否存在該檔案。
@RestController
@RequestMapping

(

"/file"

)

publicclassFileController

{

@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>
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>

秒傳

</title>
<scriptsrc="spark-md5.js"></script>
</head>
<body>
<inputtype="file"id="fileInput" />
<buttononclick="startUpload()">

開始上傳

</button>
<hr>
<script>
asyncfunctionstartUpload() 

{

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);

        }

    }

functioncalculateMd5(file

{

returnnewPromise

(

(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>

前端分為兩個步驟:
  1. 計算檔案的 MD5 值,計算之後傳送給服務端確定檔案是否存在。
  2. 如果檔案已經存在,則不需要繼續上傳檔案;如果檔案不存在,則開始上傳檔案,上傳檔案和 MD5 校驗請求類似,上面的案例程式碼中我就沒有重複演示了,松哥在書裡和之前的課程裡都多次講過檔案上傳,這裡不再囉嗦。

四 分片上傳實戰

分片上傳關鍵是在前端對檔案切片,比如一個 10MB 的檔案切為 10 份,每份 1MB。每次上傳的時候,需要多一個引數記錄當前上傳的檔案切片的起始位置。
比如一個 10MB 的檔案,切為 10 份,每份 1MB,那麼:
  • 第 0 片,從 0 開始,一共是 1024*1024 個位元組。
  • 第 1 片,從 1024*1024 開始,一共是 1024*1024 個位元組。
  • 第 2 片…
把這個搞懂,後面的程式碼就好理解了。

後端實現

privatestaticfinal

 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>
<htmllang="en">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<title>

分片示例

</title>
</head>
<body>
<inputtype="file"id="fileInput" />
<buttononclick="startUpload()">

開始上傳

</button>

<script>
asyncfunctionstartUpload() 

{

const

 fileInput = 

document

.getElementById(

'fileInput'

);

const

 file = fileInput.files[

0

];

if

 (!file) {

                alert(

"請選擇檔案"

);

return

;

            }

const

 filename = file.name;

let

 start = 

0

;
            uploadFile(file, start);

        }

asyncfunctionuploadFile(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>

五 斷點續傳實戰

斷點續傳的技術原理類似於分片上傳。
當檔案已經上傳了一部分之後,斷了需要重新開始上傳。
那麼我們的思路是這樣的:
  1. 前端先發送一個請求,檢查要上傳的檔案在服務端是否已經存在,如果存在,目前大小是多少。
  2. 前端根據已經存在的大小,繼續上傳檔案即可。

後端案例

先來看後端檢查的介面,如下:
@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>
<htmllang="en">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<title>

斷點續傳示例

</title>
</head>
<body>
<inputtype="file"id="fileInput"/>
<buttononclick="startUpload()">

開始上傳

</button>

<script>
asyncfunctionstartUpload() 

{

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);

    }

asyncfunctioncheckFile(filename

{

const

 response = 

await

 fetch(

`/file/check?filename=${filename}`

);

const

 start = 

await

 response.json();

return

 start;

    }

asyncfunctionuploadFile(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>

這個案例實際上是一個斷點續傳+分片上傳的案例,相關知識點並不難,小夥伴們可以自行體會下。

六 總結

好了,以上就是關於檔案上傳中秒傳、斷點續傳和分片上傳的實戰分享。透過這些技術的應用,我們可以極大地提升檔案上傳的效率和穩定性,改善使用者體驗。希望各位小夥伴在自己的專案中也能靈活運用這些技巧,解決實際問題。

歡迎加入我的知識星球,全面提升技術能力。
👉 加入方式,長按”或“掃描”下方二維碼噢
星球的內容包括:專案實戰、面試招聘、原始碼解析、學習路線。
文章有幫助的話,在看,轉發吧。
謝謝支援喲 (*^__^*)

相關文章