Java的分片上傳功能的實(shí)現(xiàn)
起因:最近在工作中接到了一個(gè)大文件上傳下載的需求,要求將文件上傳到share盤中,下載的時(shí)候根據(jù)前端傳的不同條件對(duì)單個(gè)或多個(gè)文件進(jìn)行打包并設(shè)置目錄下載。
一開始我想著就還是用老辦法直接file.transferTo(newFile)就算是大文件,我只要慢慢等總會(huì)傳上去的。
(原諒我的無知。。)后來嘗試之后發(fā)現(xiàn)真的是異想天開了,如果直接用普通的上傳方式基本上就會(huì)遇到以下4個(gè)問題:
- 文件上傳超時(shí):原因是前端請(qǐng)求框架限制最大請(qǐng)求時(shí)長(zhǎng),后端設(shè)置了接口訪問的超時(shí)時(shí)間,或者是 nginx(或其它代理/網(wǎng)關(guān)) 限制了最大請(qǐng)求時(shí)長(zhǎng)。
- 文件大小超限:原因在于后端對(duì)單個(gè)請(qǐng)求大小做了限制,一般 nginx 和 server 都會(huì)做這個(gè)限制。
- 上傳時(shí)間過久(想想10個(gè)g的文件上傳,這不得花個(gè)幾個(gè)小時(shí)的時(shí)間)
- 由于各種網(wǎng)絡(luò)原因上傳失敗,且失敗之后需要從頭開始。
所以我只能尋求切片上傳的幫助了。
整體思路
前端根據(jù)代碼中設(shè)置好的分片大小將上傳的文件切成若干個(gè)小文件,分多次請(qǐng)求依次上傳,后端再將文件碎片拼接為一個(gè)完整的文件,即使某個(gè)碎片上傳失敗,也不會(huì)影響其它文件碎片,只需要重新上傳失敗的部分就可以了。而且多個(gè)請(qǐng)求一起發(fā)送文件,提高了傳輸速度的上限。
(前端切片的核心是利用 Blob.prototype.slice 方法,和數(shù)組的 slice 方法相似,文件的 slice 方法可以返回原文件的某個(gè)切片)
接下來就是上代碼!
前端代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- 引入 Vue -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6/dist/vue.min.js"></script>
<!-- 引入樣式 -->
<link rel="stylesheet" rel="external nofollow" >
<!-- 引入組件庫(kù) -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<title>分片上傳測(cè)試</title>
</head>
<body>
<div id="app">
<template>
<div>
<input type="file" @change="handleFileChange" />
<el-button @click="handleUpload">上傳</el-button>
</div>
</template>
</div>
</body>
</html>
<script>
// 切片大小
// the chunk size
const SIZE = 50 * 1024 * 1024;
var app = new Vue({
el: '#app',
data: {
container: {
file: null
},
data: [],
fileListLong: '',
fileSize:''
},
methods: {
handleFileChange(e) {
const [file] = e.target.files;
if (!file) return;
this.fileSize = file.size;
Object.assign(this.$data, this.$options.data());
this.container.file = file;
},
async handleUpload() { },
// 生成文件切片
createFileChunk(file, size = SIZE) {
const fileChunkList = [];
let cur = 0;
while (cur < file.size) {
fileChunkList.push({ file: file.slice(cur, cur + size) });
cur += size;
}
return fileChunkList;
},
// 上傳切片
async uploadChunks() {
const requestList = this.data
.map(({ chunk, hash }) => {
const formData = new FormData();
formData.append("file", chunk);
formData.append("hash", hash);
formData.append("filename", this.container.file.name);
return { formData };
})
.map(({ formData }) =>
this.request({
url: "http://localhost:8080/file/upload",
data: formData
})
);
// 并發(fā)請(qǐng)求
await Promise.all(requestList);
console.log(requestList.size);
this.fileListLong = requestList.length;
// 合并切片
await this.mergeRequest();
},
async mergeRequest() {
await this.request({
url: "http://localhost:8080/file/merge",
headers: {
"content-type": "application/json"
},
data: JSON.stringify({
fileSize: this.fileSize,
fileNum: this.fileListLong,
filename: this.container.file.name
})
});
},
async handleUpload() {
if (!this.container.file) return;
const fileChunkList = this.createFileChunk(this.container.file);
this.data = fileChunkList.map(({ file }, index) => ({
chunk: file,
// 文件名 + 數(shù)組下標(biāo)
hash: this.container.file.name + "-" + index
}));
await this.uploadChunks();
},
request({
url,
method = "post",
data,
headers = {},
requestList
}) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
Object.keys(headers).forEach(key =>
xhr.setRequestHeader(key, headers[key])
);
xhr.send(data);
xhr.onload = e => {
resolve({
data: e.target.response
});
};
});
}
}
});
</script>考慮到方便和通用性,這里沒有用第三方的請(qǐng)求庫(kù),而是用原生 XMLHttpRequest 做一層簡(jiǎn)單的封裝來發(fā)請(qǐng)求
當(dāng)點(diǎn)擊上傳按鈕時(shí),會(huì)調(diào)用 createFileChunk 將文件切片,切片數(shù)量通過文件大小控制,這里設(shè)置 50MB,也就是說一個(gè) 100 MB 的文件會(huì)被分成 2 個(gè) 50MB 的切片
createFileChunk 內(nèi)使用 while 循環(huán)和 slice 方法將切片放入 fileChunkList 數(shù)組中返回
在生成文件切片時(shí),需要給每個(gè)切片一個(gè)標(biāo)識(shí)作為 hash,這里暫時(shí)使用文件名 + 下標(biāo),這樣后端可以知道當(dāng)前切片是第幾個(gè)切片,用于之后的合并切片
隨后調(diào)用 uploadChunks 上傳所有的文件切片,將文件切片,切片 hash,以及文件名放入 formData 中,再調(diào)用上一步的 request 函數(shù)返回一個(gè) proimise,最后調(diào)用 Promise.all 并發(fā)上傳所有的切片
后端代碼
實(shí)體類
@Data
public class FileUploadReq implements Serializable {
? ? private static final long serialVersionUID = 4248002065970982984L;
? ??
?? ?//切片的文件
? ? private MultipartFile file;
? ??
?? ?//切片的文件名稱
? ? private String hash;
? ??
?? ?//原文件名稱
? ? private ?String filename;
}
@Data
public class FileMergeReq implements Serializable {
? ? private static final long serialVersionUID = 3667667671957596931L;
?? ?
?? ?//文件名
? ? private String filename;
?? ?//切片數(shù)量
? ? private int fileNum;
?? ?//文件大小
? ? private String fileSize;
}@Slf4j
@CrossOrigin
@RestController
@RequestMapping("/file")
public class FileController {
? ? final String folderPath = System.getProperty("user.dir") + "/src/main/resources/static/file";
? ? @RequestMapping(value = "upload", method = RequestMethod.POST)
? ? public Object upload(FileUploadReq fileUploadEntity) {
? ? ? ? File temporaryFolder = new File(folderPath);
? ? ? ? File temporaryFile = new File(folderPath + "/" + fileUploadEntity.getHash());
? ? ? ? //如果文件夾不存在則創(chuàng)建
? ? ? ? if (!temporaryFolder.exists()) {
? ? ? ? ? ? temporaryFolder.mkdirs();
? ? ? ? }
? ? ? ? //如果文件存在則刪除
? ? ? ? if (temporaryFile.exists()) {
? ? ? ? ? ? temporaryFile.delete();
? ? ? ? }
? ? ? ? MultipartFile file = fileUploadEntity.getFile();
? ? ? ? try {
? ? ? ? ? ? file.transferTo(temporaryFile);
? ? ? ? } catch (IOException e) {
? ? ? ? ? ? log.error(e.getMessage());
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? ? ? return "success";
? ? }
? ? @RequestMapping(value = "/merge", method = RequestMethod.POST)
? ? public Object merge(@RequestBody FileMergeReq fileMergeEntity) {
? ? ? ? String finalFilename = fileMergeEntity.getFilename();
? ? ? ? File folder = new File(folderPath);
? ? ? ? //獲取暫存切片文件的文件夾中的所有文件
? ? ? ? File[] files = folder.listFiles();
? ? ? ? //合并的文件
? ? ? ? File finalFile = new File(folderPath + "/" + finalFilename);
? ? ? ? String finalFileMainName = finalFilename.split("\\.")[0];
? ? ? ? InputStream inputStream = null;
? ? ? ? OutputStream outputStream = null;
? ? ? ? try {
? ? ? ? ? ? outputStream = new FileOutputStream(finalFile, true);
? ? ? ? ? ? List<File> list = new ArrayList<>();
? ? ? ? ? ? for (File file : files) {
? ? ? ? ? ? ? ? String filename = FileNameUtil.mainName(file);
? ? ? ? ? ? ? ? //判斷是否是所需要的切片文件
? ? ? ? ? ? ? ? if (StringUtils.equals(filename, finalFileMainName)) {
? ? ? ? ? ? ? ? ? ? list.add(file);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? //如果服務(wù)器上的切片數(shù)量和前端給的數(shù)量不匹配
? ? ? ? ? ? if (fileMergeEntity.getFileNum() != list.size()) {
? ? ? ? ? ? ? ? return "文件缺失,請(qǐng)重新上傳";
? ? ? ? ? ? }
? ? ? ? ? ? //根據(jù)切片文件的下標(biāo)進(jìn)行排序
? ? ? ? ? ? List<File> fileListCollect = list.parallelStream().sorted(((file1, file2) -> {
? ? ? ? ? ? ? ? String filename1 = FileNameUtil.extName(file1);
? ? ? ? ? ? ? ? String filename2 = FileNameUtil.extName(file2);
? ? ? ? ? ? ? ? return filename1.compareTo(filename2);
? ? ? ? ? ? })).collect(Collectors.toList());
? ? ? ? ? ? //根據(jù)排序的順序依次將文件合并到新的文件中
? ? ? ? ? ? for (File file : fileListCollect) {
? ? ? ? ? ? ? ? inputStream = new FileInputStream(file);
? ? ? ? ? ? ? ? int temp = 0;
? ? ? ? ? ? ? ? byte[] byt = new byte[2 * 1024 * 1024];
? ? ? ? ? ? ? ? while ((temp = inputStream.read(byt)) != -1) {
? ? ? ? ? ? ? ? ? ? outputStream.write(byt, 0, temp);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? outputStream.flush();
? ? ? ? ? ? }
? ? ? ? } catch (FileNotFoundException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? } catch (IOException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }finally {
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? if (inputStream != null){
? ? ? ? ? ? ? ? ? ? inputStream.close();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? } catch (IOException e) {
? ? ? ? ? ? ? ? e.printStackTrace();
? ? ? ? ? ? }
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? if (outputStream != null){
? ? ? ? ? ? ? ? ? ? outputStream.close();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? } catch (IOException e) {
? ? ? ? ? ? ? ? e.printStackTrace();
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? // 產(chǎn)生的文件大小和前端一開始上傳的文件不一致
? ? ? ? if (finalFile.length() != Long.parseLong(fileMergeEntity.getFileSize())) {
? ? ? ? ? ? return "上傳文件大小不一致";
? ? ? ? }
? ? ? ? return "上傳成功";
? ? }
}為了圖方便我就直接return 字符串了 嘿嘿(當(dāng)然我在這個(gè)demo里面寫了方法統(tǒng)一結(jié)果的封裝,所以輸出的時(shí)候還是restful風(fēng)格的結(jié)果,詳細(xì)內(nèi)容可以看我之前的文章《Spring使用AOP完成統(tǒng)一結(jié)果封裝》)
當(dāng)前端調(diào)用upload接口的時(shí)候,后端就會(huì)將前端傳過來的文件放到一個(gè)臨時(shí)文件夾中
當(dāng)調(diào)用merge接口的時(shí)候,后端就會(huì)認(rèn)為分片文件已經(jīng)全部上傳完畢就會(huì)進(jìn)行文件合并的工作
后端主要是根據(jù)前端返回的hash值來判斷分片文件的順序
結(jié)尾
其實(shí)分片上傳聽起來好像很麻煩,其實(shí)只要把思路捋清楚了其實(shí)是不難的,是一個(gè)比較簡(jiǎn)單的需求。
當(dāng)然這個(gè)只是一個(gè)比較簡(jiǎn)單一個(gè)demo,只是實(shí)現(xiàn)的一個(gè)較為簡(jiǎn)單的分片上傳功能,像斷點(diǎn)上傳,上傳暫停這些功能暫時(shí)還沒來得及寫到demo里面,之后有時(shí)間了會(huì)新開一個(gè)文章寫這些額外的內(nèi)容。
到此這篇關(guān)于Java的分片上傳功能的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Java 分片上傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring事務(wù)失效場(chǎng)景的詳細(xì)整理
Spring 事務(wù)的傳播特性說的是,當(dāng)多個(gè)事務(wù)同時(shí)存在的時(shí)候,Spring 如何處理這些事務(wù)的特性,下面這篇文章主要給大家介紹了關(guān)于Spring事務(wù)失效場(chǎng)景的相關(guān)資料,需要的朋友可以參考下2022-02-02
Springcloud中的@RefreshScope的實(shí)現(xiàn)
@RefreshScope注解是Spring Cloud中的一個(gè)注解,它用來實(shí)現(xiàn)Bean中屬性的動(dòng)態(tài)刷新,本文就來介紹一下@RefreshScope注解的使用,感興趣的可以了解一下2024-06-06
Java通過PropertyDescriptor反射調(diào)用set和get方法
這篇文章主要為大家詳細(xì)介紹了Java通過PropertyDescriptor反射調(diào)用set和get方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
Java使用poi實(shí)現(xiàn)excel的導(dǎo)入操作指南
使用Apache Poi是一種流行且廣泛使用的方式,可以幫助開發(fā)人員直接從Java代碼中讀取、寫入和處理Excel文件,因此在這篇文章我們將著重介紹如何實(shí)現(xiàn)excel的導(dǎo)入,感興趣的朋友可以跟著小編一起來學(xué)習(xí)2023-06-06
java讀取html文件,并獲取body中所有的標(biāo)簽及內(nèi)容的案例
這篇文章主要介紹了java讀取html文件,并獲取body中所有的標(biāo)簽及內(nèi)容的案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08

