Java實現(xiàn)大文件的分片上傳與下載(springboot+vue3)
源碼:
- https://gitee.com/gaode-8/big-file-upload
演示視頻
- https://www.bilibili.com/video/BV1CA411f7np/?vd_source=1fe29350b37642fa583f709b9ae44b35
1.1 項目背景
對于超大文件上傳我們可能遇到以下問題
- • 大文件直接上傳,占用過多內存,可能導致內存溢出甚至系統(tǒng)崩潰
- • 受網(wǎng)絡環(huán)境影響,可能導致傳輸中斷,只能重新傳輸
- • 傳輸時間長,用戶無法知道傳輸進度,用戶體驗不佳
1.2 項目目標
對于上述問題,我們需要對文件做分片傳輸。分片傳輸就是把文件分割成許多較小的文件,然后分多次上傳,最后再完成合并。
受網(wǎng)絡環(huán)境影響,我們還要實現(xiàn)斷點續(xù)傳,以節(jié)省傳輸時間和資源。斷點續(xù)傳就是已經(jīng)上傳或者下載過的文件分片不再傳輸。
對于已經(jīng)上傳過的文件,可以不再上傳,實現(xiàn)秒傳。秒傳就是根據(jù)文件的唯一標識,確認是否需要上傳。
實現(xiàn)多任務上傳或下載。多任務就是同時多個文件上傳或下載。
2.1 業(yè)務流程
用戶上傳文件的流程圖如圖1所示,用戶首先選擇要上傳的文件,上傳過程中可以選擇暫?;蚶^續(xù)上傳。

用戶上傳文件的流程圖如圖2所示,用戶首先可以瀏覽可以下載的文件列表,然后點擊下載,下載過程中可以選擇暫?;蚶^續(xù)下載

2.2 系統(tǒng)用例
系統(tǒng)用例圖如圖3所示,用戶可以上傳文件,在文件上傳過程中可以查看文件的上傳進度和速度,也可以暫停或開始上傳;用戶可以查看已經(jīng)上傳過的,也就是可以下載的文件列表;用戶可以下載文件,在下載過程中可以查看文件下載的速度和進度,用戶可以暫?;蜷_始下載。

2.3 系統(tǒng)總體功能
系統(tǒng)總體功能圖如圖4所示,分為上傳和下載。上傳包括秒傳,分片上傳,斷點續(xù)傳,多任務。下載包括分片下載,斷點續(xù)傳,多任務。

3.1 技術選型
后端:
• 語言:Java8
• 框架:SpringBoot2.6
• 開發(fā)工具:Idea 2021
前端:
• 語言:Html5、css3、JavaScript
• 框架:Vue3
• 開發(fā)工具:Vscode、Edge
數(shù)據(jù)庫:
• mysql8
4.1 文件上傳模塊
文件上傳模塊的流程圖如圖6所示,順序圖如圖7所示
首先前端讀取文件生成文件的唯一標識MD5,這里采用常用的MD5生成框架:spark-md5.js。對于大文件一次性讀取比較慢,而且容易造成瀏覽器崩潰,因此這里采用分片讀取的方式計算MD5。
然后向服務器發(fā)送請求,查看該文件時候已經(jīng)上傳,如果已經(jīng)上傳,就提示用戶已經(jīng)秒傳。
如果數(shù)據(jù)庫中沒有記錄該文件,就表示該文件沒有上傳或沒有上傳完成,那么服務器就查詢并返回記錄的chunk分片列表。
async 和 await配可以實現(xiàn)等待異步函數(shù)計算完成
//計算文件的md5值
function computeMd5(file, uploadFile) {
return new Promise((resolve, reject) => {
//分片讀取并計算md5
const chunkTotal = 100; //分片數(shù)
const chunkSize = Math.ceil(file.size / chunkTotal);
const fileReader = new FileReader();
const md5 = new SparkMD5();
let index = 0;
const loadFile = (uploadFile) => {
uploadFile.parsePercentage.value = parseInt((index / file.size) * 100);
const slice = file.slice(index, index + chunkSize);
fileReader.readAsBinaryString(slice);
};
loadFile(uploadFile);
fileReader.onload = (e) => {
md5.appendBinary(e.target.result);
if (index < file.size) {
index += chunkSize;
loadFile(uploadFile);
} else {
// md5.end() 就是文件md5碼
resolve(md5.end());
}
};
});
}
//檢查文件是否存在
function checkFile(md5) {
return request({
url: "/check",
method: "get",
params: {
md5: md5,
},
});
}
//文件上傳之前,el-upload自動觸發(fā)
async function beforeUpload(file) {
console.log("2.上傳文件之前");
var uploadFile = {};
uploadFile.name = file.name;
uploadFile.size = file.size;
uploadFile.parsePercentage = ref(0);
uploadFile.uploadPercentage = ref(0);
uploadFile.uploadSpeed = "0 M/s";
uploadFile.chunkList = null;
uploadFile.file = file;
uploadFile.uploadingStop = false;
uploadFileList.value.push(uploadFile);
var md5 = await computeMd5(file, uploadFile);//async 和 await配可以實現(xiàn)等待異步函數(shù)計算完成
uploadFile.md5 = md5;
var res = await checkFile(md5); //上傳服務器檢查,以確認是否秒傳
var data = res.data.data;
if (!data.isUploaded) {
uploadFile.chunkList = data.chunkList;
uploadFile.needUpload = true;
} else {
uploadFile.needUpload = false;
uploadFile.uploadPercentage.value = 100;
console.log("文件已秒傳");
ElMessage({
showClose: true,
message: "文件已秒傳",
type: "warning",
});
}
}前端分片請求文件,如果分片編號被包含在分片列表內,就標識該分片已經(jīng)上傳,跳過;反之,表示還未上傳,那么前端通過file的slice方法分割文件,向服務端傳遞。同時在頁面上顯示上傳進度和速度。
服務端,收到前端的分片文件后,通過Java的RandomAccess類(隨機讀寫類),從文件的指定位置,寫入指定字節(jié),并記錄chunk到數(shù)據(jù)庫,如果是最后一個分片再記錄file到數(shù)據(jù)庫。

圖6 文件上傳流程圖

圖7 文件上傳順序圖
前端代碼
<template>
<div class="main">
<!-- 文件上傳按鈕 -->
<el-upload
action="#"
:http-request="upload"
:before-upload="beforeUpload"
:show-file-list="false"
>
<el-button type="primary">選擇上傳文件</el-button>
</el-upload>
<el-divider content-position="left">上傳列表</el-divider>
<!-- 正在上傳的文件列表 -->
<div class="uploading" v-for="uploadFile in uploadFileList">
<span class="fileName">{{ uploadFile.name }}</span>
<span class="fileSize">{{ formatSize(uploadFile.size) }}</span>
<div class="parse">
<span>解析進度: </span>
<el-progress
:text-inside="true"
:stroke-width="16"
:percentage="uploadFile.parsePercentage"
>
</el-progress>
</div>
<div class="progress">
<span>上傳進度:</span>
<el-progress
:text-inside="true"
:stroke-width="16"
:percentage="uploadFile.uploadPercentage"
>
</el-progress>
<span
v-if="
(uploadFile.uploadPercentage > 0) &
(uploadFile.uploadPercentage < 100)
"
>
<span class="uploadSpeed">{{ uploadFile.uploadSpeed }}</span>
<el-button circle link @click="changeUploadingStop(uploadFile)">
<el-icon size="20" v-if="uploadFile.uploadingStop == false"
><VideoPause
/></el-icon>
<el-icon size="20" v-else><VideoPlay /></el-icon>
</el-button>
</span>
</div>
</div>
</div>
</template>
<script setup>
import emitter from "../utils/eventBus.js";
import { ElMessage } from "element-plus";
import SparkMD5 from "spark-md5";
import { VideoPause, VideoPlay } from "@element-plus/icons-vue";
import { ref, reactive, getCurrentInstance, nextTick } from "vue";
const { appContext } = getCurrentInstance();
const request = appContext.config.globalProperties.request;
var uploadFileList = ref([]);
//換算文件的大小單位
function formatSize(size) {
//size的單位大小k
var unit;
var units = [" B", " K", " M", " G"];
var pointLength = 2;
while ((unit = units.shift()) && size > 1024) {
size = size / 1024;
}
return (
(unit === "B"
? size
: size.toFixed(pointLength === undefined ? 2 : pointLength)) + unit
);
}
//計算文件的md5值
function computeMd5(file, uploadFile) {
return new Promise((resolve, reject) => {
//分片讀取并計算md5
const chunkTotal = 100; //分片數(shù)
const chunkSize = Math.ceil(file.size / chunkTotal);
const fileReader = new FileReader();
const md5 = new SparkMD5();
let index = 0;
const loadFile = (uploadFile) => {
uploadFile.parsePercentage.value = parseInt((index / file.size) * 100);
const slice = file.slice(index, index + chunkSize);
fileReader.readAsBinaryString(slice);
};
loadFile(uploadFile);
fileReader.onload = (e) => {
md5.appendBinary(e.target.result);
if (index < file.size) {
index += chunkSize;
loadFile(uploadFile);
} else {
// md5.end() 就是文件md5碼
resolve(md5.end());
}
};
});
}
//檢查文件是否存在
function checkFile(md5) {
return request({
url: "/check",
method: "get",
params: {
md5: md5,
},
});
}
//文件上傳之前,el-upload自動觸發(fā)
async function beforeUpload(file) {
console.log("2.上傳文件之前");
var uploadFile = {};
uploadFile.name = file.name;
uploadFile.size = file.size;
uploadFile.parsePercentage = ref(0);
uploadFile.uploadPercentage = ref(0);
uploadFile.uploadSpeed = "0 M/s";
uploadFile.chunkList = null;
uploadFile.file = file;
uploadFile.uploadingStop = false;
uploadFileList.value.push(uploadFile);
var md5 = await computeMd5(file, uploadFile);//async 和 await配可以實現(xiàn)等待異步函數(shù)計算完成
uploadFile.md5 = md5;
var res = await checkFile(md5); //上傳服務器檢查,以確認是否秒傳
var data = res.data.data;
if (!data.isUploaded) {
uploadFile.chunkList = data.chunkList;
uploadFile.needUpload = true;
} else {
uploadFile.needUpload = false;
uploadFile.uploadPercentage.value = 100;
console.log("文件已秒傳");
ElMessage({
showClose: true,
message: "文件已秒傳",
type: "warning",
});
}
}
//點擊暫?;蜷_始上傳
function changeUploadingStop(uploadFile) {
uploadFile.uploadingStop = !uploadFile.uploadingStop;
if (!uploadFile.uploadingStop) {
uploadChunk(uploadFile.file, 1, uploadFile);
}
}
//上傳文件,替換el-upload的action
function upload(xhrData) {
var uploadFile = null;
for (var i = 0; i < uploadFileList.value.length; i++) {
if (
(xhrData.file.name == uploadFileList.value[i].name) &
(xhrData.file.size == uploadFileList.value[i].size)
) {
uploadFile = uploadFileList.value[i];
break;
}
}
if (uploadFile.needUpload) {
console.log("3.上傳文件");
// 分片上傳文件
// 確定分片的大小
uploadChunk(xhrData.file, 1, uploadFile);
}
}
//上傳文件分片
function uploadChunk(file, index, uploadFile) {
var chunkSize = 1024 * 1024 * 10; //10mb
var chunkTotal = Math.ceil(file.size / chunkSize);
if (index <= chunkTotal) {
// 根據(jù)是否暫停,確定是否繼續(xù)上傳
// console.log("4.上傳分片");
var startTime = new Date().valueOf();
var exit = uploadFile.chunkList.includes(index);
// console.log("是否存在",exit);
if (!exit) {
// console.log("3.3上傳文件",uploadingStop);
if (!uploadFile.uploadingStop) {
// 分片上傳,同時計算進度條和上傳速度
// 已經(jīng)上傳的不在上傳、
// 上傳完成后提示,上傳成功
// console.log("上傳分片1",index);
var form = new FormData();
var start = (index - 1) * chunkSize;
let end =
index * chunkSize >= file.size ? file.size : index * chunkSize;
let chunk = file.slice(start, end);
// downloadBlob(chunk,file)
// console.log("chunk",chunk);
form.append("chunk", chunk);
form.append("index", index);
form.append("chunkTotal", chunkTotal);
form.append("chunkSize", chunkSize);
form.append("md5", uploadFile.md5);
form.append("fileSize", file.size);
form.append("fileName", file.name);
// console.log("上傳分片", index);
request({
url: "/upload/chunk",
method: "post",
data: form,
}).then((res) => {
var endTime = new Date().valueOf();
var timeDif = (endTime - startTime) / 1000;
// console.log("上傳文件大小",formatSize(chunkSize));
// console.log("耗時",timeDif);
// console.log("then",index);
// uploadSpeed = (chunkSize/(1024*1024)) / timeDif +" M / s"
uploadFile.uploadSpeed = (10 / timeDif).toFixed(1) + " M/s";
// console.log(res.data.data);
// console.log("f2",uploadFile);
uploadFile.chunkList.push(index);
// console.log("f3",uploadFile);
uploadFile.uploadPercentage = parseInt(
(uploadFile.chunkList.length / chunkTotal) * 100
);
// console.log("上傳進度",uploadFile.uploadPercentage);
if (index == chunkTotal) {
emitter.emit("reloadFileList");
}
uploadChunk(file, index + 1, uploadFile);
});
}
} else {
uploadFile.uploadPercentage = parseInt(
(uploadFile.chunkList.length / chunkTotal) * 100
);
uploadChunk(file, index + 1, uploadFile);
}
// }
}
}
</script>
<style scoped>
.main {
margin-top: 40px;
margin-bottom: 40px;
}
.uploading {
padding-top: 27px;
}
.progress {
/* width: 700px; */
display: flex;
}
.uploading .parse {
display: flex;
}
.parse .el-progress {
/* font-size: 18px; */
width: 590px;
}
.progress .el-progress {
/* font-size: 18px; */
width: 590px;
}
.uploading .fileName {
font-size: 17px;
margin-right: 40px;
margin-left: 80px;
/* width: 80px; */
}
.uploading .fileSize {
font-size: 17px;
/* width: 80px; */
}
.progress .uploadSpeed {
font-size: 17px;
margin-left: 5px;
padding-left: 5px;
padding-right: 10px;
}
</style>后端代碼
package com.cugb.bigfileupload.controller;
import com.cugb.bigfileupload.bean.FilePO;
import com.cugb.bigfileupload.bean.Result;
import com.cugb.bigfileupload.servie.ChunkService;
import com.cugb.bigfileupload.servie.FileService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@RestController
@CrossOrigin
public class FileController {
Logger logger = LoggerFactory.getLogger(getClass());
@Value("${file.path}")
private String filePath;
@Autowired
private FileService fileService;
@Autowired
private ChunkService chunkService;
@GetMapping("/check")
public Result checkFile(@RequestParam("md5") String md5){
logger.info("檢查MD5:"+md5);
//首先檢查是否有完整的文件
Boolean isUploaded = fileService.selectFileByMd5(md5);
Map<String, Object> data = new HashMap<>();
data.put("isUploaded",isUploaded);
//如果有,就返回秒傳
if(isUploaded){
return new Result(201,"文件已經(jīng)秒傳",data);
}
//如果沒有,就查找分片信息,并返回給前端
List<Integer> chunkList = chunkService.selectChunkListByMd5(md5);
data.put("chunkList",chunkList);
return new Result(201,"",data);
}
@PostMapping("/upload/chunk")
public Result uploadChunk(@RequestParam("chunk") MultipartFile chunk,
@RequestParam("md5") String md5,
@RequestParam("index") Integer index,
@RequestParam("chunkTotal")Integer chunkTotal,
@RequestParam("fileSize")Long fileSize,
@RequestParam("fileName")String fileName,
@RequestParam("chunkSize")Long chunkSize
){
String[] splits = fileName.split("\\.");
String type = splits[splits.length-1];
String resultFileName = filePath+md5+"."+type;
chunkService.saveChunk(chunk,md5,index,chunkSize,resultFileName);
logger.info("上傳分片:"+index +" ,"+chunkTotal+","+fileName+","+resultFileName);
if(Objects.equals(index, chunkTotal)){
FilePO filePO = new FilePO(fileName, md5, fileSize);
fileService.addFile(filePO);
chunkService.deleteChunkByMd5(md5);
return new Result(200,"文件上傳成功",index);
}else{
return new Result(201,"分片上傳成功",index);
}
}
@GetMapping("/fileList")
public Result getFileList(){
logger.info("查詢文件列表");
List<FilePO> fileList = fileService.selectFileList();
return new Result(201,"文件列表查詢成功",fileList);
}
}4.2 文件下載模塊
文件下載的流程圖如圖8所示,順序圖如圖9所示
文件下載是首先,前端向后端發(fā)送分片下載的請求,請求的responseType設為blob(Binary large Object) ,然后后端通過RandomAccess類讀取指定字節(jié)的內容,再寫入到響應的文件流中。
瀏覽器前端的請求的分片數(shù)據(jù),會暫時保存在“C:\Users\用戶名\AppData\Local\Microsoft\Edge\User Data\Default\blob_storage\”中,(請確保c盤有足夠的空間),當所有分片下載完成,會合并成一個大文件(很快),分片不是放在內存中,所以不用擔心文件太大是不是不行。
,
刷新瀏覽器,也會刪除已經(jīng)下載好的分片
當前端請求了所有的文件分片之后,再把所有的blob合并成一個blob
if (index == chunkTotal) {
var resBlob = new Blob(file.blobList, {
type: "application/octet-stream",
});
// console.log("resb", resBlob);
let url = window.URL.createObjectURL(resBlob); // 將獲取的文件轉化為blob格式
let a = document.createElement("a"); // 此處向下是打開一個儲存位置
a.style.display = "none";
a.href = url;
// 下面兩行是自己項目需要的處理,總之就是得到下載的文件名(加后綴)即可
var fileName = file.name;
a.setAttribute("download", fileName);
document.body.appendChild(a);
a.click(); //點擊下載
document.body.removeChild(a); // 下載完成移除元素
window.URL.revokeObjectURL(url); // 釋放掉blob對象
}

圖9文件上傳順序圖
前端代碼
<template>
<div class="main">
<div class="fileList">
<div class="title">
文件列表
<!-- <hr> -->
</div>
<el-table :data="fileList" border style="width: 360px">
<el-table-column prop="name" label="文件名" width="150">
</el-table-column>
<el-table-column prop="size" label="文件大小" width="110">
<template #default="scope">
{{ formatSize(scope.row) }}
</template>
</el-table-column>
<el-table-column prop="" label="操作" width="100">
<template #default="scope">
<el-button
size="small"
type="primary"
@click="downloadFile(scope.row)"
>下載</el-button
>
</template>
</el-table-column>
</el-table>
</div>
<div class="downloadList">
<el-divider content-position="left">下載列表</el-divider>
<div v-for="file in downloadingFileList">
<div class="downloading">
<span class="fileName">{{ file.name }}</span>
<span class="fileSize">{{ formatSize(file) }}</span>
<span class="downloadSpeed">{{ file.downloadSpeed }}</span>
<div class="progress">
<span>下載進度:</span>
<el-progress
:text-inside="true"
:stroke-width="16"
:percentage="file.downloadPersentage"
>
</el-progress>
<el-button circle link @click="changeDownloadStop(file)">
<el-icon size="20" v-if="file.downloadingStop == false"
><VideoPause
/></el-icon>
<el-icon size="20" v-else><VideoPlay /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import axios from "axios";
import { ref, reactive, getCurrentInstance } from "vue";
import emitter from "../utils/eventBus.js";
import { VideoPause, VideoPlay } from "@element-plus/icons-vue";
const { appContext } = getCurrentInstance();
const request = appContext.config.globalProperties.request;
var fileList = reactive([]);
var downloadingFileList = ref([]);
//上傳文件之后,重新加載文件列表
emitter.on("reloadFileList", () => {
load();
});
function load() {
fileList.length = 0;
request({
url: "/fileList",
method: "get",
}).then((res) => {
// console.log("res", res.data.data);
fileList.push(...res.data.data);
});
}
load();
//換算文件的大小單位
function formatSize(file) {
//console.log("size",file.size);
var size = file.size;
var unit;
var units = [" B", " K", " M", " G"];
var pointLength = 2;
while ((unit = units.shift()) && size > 1024) {
size = size / 1024;
}
return (
(unit === "B"
? size
: size.toFixed(pointLength === undefined ? 2 : pointLength)) + unit
);
}
//點擊暫停下載
function changeDownloadStop(file) {
file.downloadingStop = !file.downloadingStop;
if (!file.downloadingStop) {
console.log("開始。。");
downloadChunk(1, file);
}
}
//點擊下載文件
function downloadFile(file) {
// console.log("下載", file);
file.downloadingStop = false;
file.downloadSpeed = "0 M/s";
file.downloadPersentage = 0;
file.blobList = [];
file.chunkList = [];
downloadingFileList.value.push(file);
downloadChunk(1, file);
}
//點擊下載文件分片
function downloadChunk(index, file) {
var chunkSize = 1024 * 1024 * 5;
var chunkTotal = Math.ceil(file.size / chunkSize);
if (index <= chunkTotal) {
// console.log("下載進度",index);
var exit = file.chunkList.includes(index);
console.log("存在", exit);
if (!exit) {
if (!file.downloadingStop) {
var formData = new FormData();
formData.append("fileName", file.name);
formData.append("md5", file.md5);
formData.append("chunkSize", chunkSize);
formData.append("index", index);
formData.append("chunkTotal", chunkTotal);
if (index * chunkSize >= file.size) {
chunkSize = file.size - (index - 1) * chunkSize;
formData.set("chunkSize", chunkSize);
}
var startTime = new Date().valueOf();
axios({
url: "http://localhost:9001/download",
method: "post",
data: formData,
responseType: "blob",
timeout: 50000,
}).then((res) => {
file.chunkList.push(index);
var endTime = new Date().valueOf();
var timeDif = (endTime - startTime) / 1000;
file.downloadSpeed = (5 / timeDif).toFixed(1) + " M/s";
//todo
file.downloadPersentage = parseInt((index / chunkTotal) * 100);
// var chunk = res.data.data.chunk
// const blob = new Blob([res.data]);
const blob = res.data;
file.blobList.push(blob);
// console.log("res", blobList);
if (index == chunkTotal) {
var resBlob = new Blob(file.blobList, {
type: "application/octet-stream",
});
// console.log("resb", resBlob);
let url = window.URL.createObjectURL(resBlob); // 將獲取的文件轉化為blob格式
let a = document.createElement("a"); // 此處向下是打開一個儲存位置
a.style.display = "none";
a.href = url;
// 下面兩行是自己項目需要的處理,總之就是得到下載的文件名(加后綴)即可
var fileName = file.name;
a.setAttribute("download", fileName);
document.body.appendChild(a);
a.click(); //點擊下載
document.body.removeChild(a); // 下載完成移除元素
window.URL.revokeObjectURL(url); // 釋放掉blob對象
}
downloadChunk(index + 1, file);
});
}
} else {
file.downloadPersentage = parseInt((index / chunkTotal) * 100);
downloadChunk(index + 1, file);
}
}
}
</script>
<style scoped>
.main {
display: flex;
}
.fileList {
width: 400px;
}
.downloadList {
width: 450px;
}
.title {
margin-top: 5px;
margin-bottom: 5px;
}
.downloading {
margin-top: 10px;
}
.downloading .fileName {
margin-left: 76px;
margin-right: 30px;
}
.downloading .fileSize {
/* margin-left: 70px; */
margin-right: 30px;
}
.downloading .progress {
display: flex;
}
.progress .el-progress {
/* font-size: 18px; */
width: 310px;
}
</style>后端代碼
package com.cugb.bigfileupload.controller;
import com.cugb.bigfileupload.servie.ChunkService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
@Controller
@CrossOrigin
public class DownLoadController {
Logger logger = LoggerFactory.getLogger(getClass());
@Value("${file.path}")
private String filePath;
@Autowired
private ChunkService chunkService;
@PostMapping("/download")
public void download(@RequestParam("md5") String md5,
@RequestParam("fileName") String fileName,
@RequestParam("chunkSize") Integer chunkSize,
@RequestParam("chunkTotal") Integer chunkTotal,
@RequestParam("index")Integer index,
HttpServletResponse response) {
String[] splits = fileName.split("\\.");
String type = splits[splits.length - 1];
String resultFileName = filePath + md5 + "." + type;
File resultFile = new File(resultFileName);
long offset = (long) chunkSize * (index - 1);
if(Objects.equals(index, chunkTotal)){
offset = resultFile.length() -chunkSize;
}
byte[] chunk = chunkService.getChunk(index, chunkSize, resultFileName,offset);
logger.info("下載文件分片" + resultFileName + "," + index + "," + chunkSize + "," + chunk.length+","+offset);
// response.addHeader("Access-Control-Allow-Origin","Content-Disposition");
response.addHeader("Content-Disposition", "attachment;filename=" + fileName);
response.addHeader("Content-Length", "" + (chunk.length));
response.setHeader("filename", fileName);
response.setContentType("application/octet-stream");
ServletOutputStream out = null;
try {
out = response.getOutputStream();
out.write(chunk);
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}4.3 數(shù)據(jù)庫設計
4.3.1 概念結構設計
數(shù)據(jù)庫設計只有倆個表,一個file表來記錄已經(jīng)完整上傳的文件信息,一個chunk表用來記錄還未上傳完成的分片信息

5.1 大文件上傳實現(xiàn)
上傳頁面如圖13所示,有一個“選擇上傳文件”的按鈕,下面是顯示正在上傳文件的列表

圖13 上傳頁面首頁
我們選擇要上傳的文件,確認上傳,首先會顯示解析進度,當解析完成后,就會開始上傳,并顯示上傳進度和速度;同時,我們可以選擇多個文件一同上傳;在上傳的同時我們還可以暫停上傳。如圖14所示

圖14 上傳文件中
當文件上傳成功之后,就會彈窗提示文件上傳成功。如圖15所示

圖15 文件上傳成功

5.2 大文件下載實現(xiàn)
文件下載頁面如圖16所示,左邊是可以下載文件的列表,右邊是下載中的文件


當所有的分片下載完成后,前端會將所有的分片合并成一個文件。如圖18所示
以上就是Java實現(xiàn)大文件的分片上傳與下載(springboot+vue3)的詳細內容,更多關于Java 大文件分片上傳與下載的資料請關注腳本之家其它相關文章!
相關文章
SpringBoot中@EnableAutoConfiguration注解源碼分析
這篇文章主要介紹了SpringBoot中@EnableAutoConfiguration注解源碼分析,@EnableAutoConfiguration,主要是用于加載Starter目錄包之外的、需要Spring自動生成Bean對象的、帶有@Configuration注解的類,需要的朋友可以參考下2023-08-08
使用Springboot 打jar包實現(xiàn)分離依賴lib和配置
這篇文章主要介紹了使用Springboot 打jar包實現(xiàn)分離依賴lib和配置方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02
布隆過濾器(Bloom Filter)的Java實現(xiàn)方法
下面小編就為大家?guī)硪黄悸∵^濾器(Bloom Filter)的Java實現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-12-12

