Java大文件上傳(分片上傳+斷點(diǎn)續(xù)傳)的解決方案詳解
什么是大文件上傳
大文件上傳通常指上傳超過幾百M(fèi)B甚至幾個(gè)GB的文件。與普通文件上傳相比,大文件上傳面臨以下挑戰(zhàn):
- 內(nèi)存限制 - 一次性加載整個(gè)文件到內(nèi)存會(huì)導(dǎo)致內(nèi)存溢出
- 網(wǎng)絡(luò)穩(wěn)定性 - 上傳過程中網(wǎng)絡(luò)中斷需要能夠斷點(diǎn)續(xù)傳
- 超時(shí)問題 - 長時(shí)間上傳可能導(dǎo)致連接超時(shí)
- 進(jìn)度監(jiān)控 - 需要實(shí)時(shí)顯示上傳進(jìn)度
- 文件校驗(yàn) - 確保文件完整性和安全性
解決方案:分片上傳
大文件上傳的核心思想是將文件分割成多個(gè)小塊,分別上傳,最后在服務(wù)器端合并。
前端代碼示例 (HTML + JavaScript)
<!DOCTYPE html>
<html>
<head>
<title>大文件上傳</title>
</head>
<body>
<input type="file" id="fileInput" />
<button onclick="uploadFile()">開始上傳</button>
<div id="progress"></div>
<script>
const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB
async function uploadFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('請選擇文件');
return;
}
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
const fileMd5 = await calculateFileMD5(file);
// 檢查文件是否已上傳過
const checkResult = await checkFileExists(file.name, fileMd5, file.size);
if (checkResult.uploaded) {
alert('文件已存在');
return;
}
let uploadedChunks = checkResult.uploadedChunks || [];
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
// 跳過已上傳的分片
if (uploadedChunks.includes(chunkIndex)) {
updateProgress(chunkIndex + 1, totalChunks);
continue;
}
const chunk = file.slice(chunkIndex * CHUNK_SIZE, (chunkIndex + 1) * CHUNK_SIZE);
const formData = new FormData();
formData.append('file', chunk);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', totalChunks);
formData.append('fileName', file.name);
formData.append('fileMd5', fileMd5);
try {
await uploadChunk(formData);
updateProgress(chunkIndex + 1, totalChunks);
} catch (error) {
console.error(`分片 ${chunkIndex} 上傳失敗:`, error);
alert('上傳失敗');
return;
}
}
// 所有分片上傳完成,請求合并
await mergeChunks(file.name, fileMd5, totalChunks);
alert('上傳完成');
}
function uploadChunk(formData) {
return fetch('/upload/chunk', {
method: 'POST',
body: formData
}).then(response => {
if (!response.ok) {
throw new Error('上傳失敗');
}
return response.json();
});
}
function checkFileExists(fileName, fileMd5, fileSize) {
return fetch(`/upload/check?fileName=${fileName}&fileMd5=${fileMd5}&fileSize=${fileSize}`)
.then(response => response.json());
}
function mergeChunks(fileName, fileMd5, totalChunks) {
return fetch('/upload/merge', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fileName: fileName,
fileMd5: fileMd5,
totalChunks: totalChunks
})
}).then(response => response.json());
}
function updateProgress(current, total) {
const progress = document.getElementById('progress');
const percentage = Math.round((current / total) * 100);
progress.innerHTML = `上傳進(jìn)度: ${percentage}%`;
}
// 計(jì)算文件MD5(簡化版,實(shí)際應(yīng)使用更可靠的庫)
async function calculateFileMD5(file) {
// 這里使用簡單的文件名+大小模擬MD5
// 實(shí)際項(xiàng)目中應(yīng)使用 spark-md5 等庫
return btoa(file.name + file.size).replace(/[^a-zA-Z0-9]/g, '');
}
</script>
</body>
</html>
后端Java代碼示例 (Spring Boot)
配置文件上傳設(shè)置
@Configuration
public class UploadConfig {
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setMaxFileSize("10GB");
factory.setMaxRequestSize("10GB");
return factory.createMultipartConfig();
}
}
文件上傳控制器
@RestController
@RequestMapping("/upload")
public class FileUploadController {
@Value("${file.upload-dir:/tmp/uploads}")
private String uploadDir;
/**
* 檢查文件是否存在
*/
@GetMapping("/check")
public ResponseEntity<CheckResult> checkFile(
@RequestParam String fileName,
@RequestParam String fileMd5,
@RequestParam Long fileSize) {
String filePath = Paths.get(uploadDir, fileMd5, fileName).toString();
File file = new File(filePath);
CheckResult result = new CheckResult();
// 如果文件已存在
if (file.exists() && file.length() == fileSize) {
result.setUploaded(true);
return ResponseEntity.ok(result);
}
// 檢查已上傳的分片
String chunkDir = getChunkDir(fileMd5);
File chunkFolder = new File(chunkDir);
if (!chunkFolder.exists()) {
result.setUploaded(false);
result.setUploadedChunks(new ArrayList<>());
return ResponseEntity.ok(result);
}
List<Integer> uploadedChunks = Arrays.stream(chunkFolder.listFiles())
.map(f -> Integer.parseInt(f.getName()))
.collect(Collectors.toList());
result.setUploaded(false);
result.setUploadedChunks(uploadedChunks);
return ResponseEntity.ok(result);
}
/**
* 上傳文件分片
*/
@PostMapping("/chunk")
public ResponseEntity<UploadResult> uploadChunk(
@RequestParam("file") MultipartFile file,
@RequestParam Integer chunkIndex,
@RequestParam Integer totalChunks,
@RequestParam String fileName,
@RequestParam String fileMd5) {
try {
// 創(chuàng)建分片目錄
String chunkDir = getChunkDir(fileMd5);
File chunkFolder = new File(chunkDir);
if (!chunkFolder.exists()) {
chunkFolder.mkdirs();
}
// 保存分片文件
File chunkFile = new File(chunkDir + File.separator + chunkIndex);
file.transferTo(chunkFile);
UploadResult result = new UploadResult();
result.setSuccess(true);
result.setMessage("分片上傳成功");
return ResponseEntity.ok(result);
} catch (Exception e) {
UploadResult result = new UploadResult();
result.setSuccess(false);
result.setMessage("分片上傳失敗: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
/**
* 合并文件分片
*/
@PostMapping("/merge")
public ResponseEntity<MergeResult> mergeChunks(@RequestBody MergeRequest request) {
try {
String chunkDir = getChunkDir(request.getFileMd5());
String fileName = request.getFileName();
String filePath = Paths.get(uploadDir, request.getFileMd5(), fileName).toString();
// 創(chuàng)建目標(biāo)文件
File targetFile = new File(filePath);
File parentDir = targetFile.getParentFile();
if (!parentDir.exists()) {
parentDir.mkdirs();
}
// 合并分片
try (FileOutputStream fos = new FileOutputStream(targetFile)) {
for (int i = 0; i < request.getTotalChunks(); i++) {
File chunkFile = new File(chunkDir + File.separator + i);
try (FileInputStream fis = new FileInputStream(chunkFile)) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
}
// 刪除分片文件
chunkFile.delete();
}
}
// 刪除分片目錄
new File(chunkDir).delete();
MergeResult result = new MergeResult();
result.setSuccess(true);
result.setMessage("文件合并成功");
result.setFilePath(filePath);
return ResponseEntity.ok(result);
} catch (Exception e) {
MergeResult result = new MergeResult();
result.setSuccess(false);
result.setMessage("文件合并失敗: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
private String getChunkDir(String fileMd5) {
return Paths.get(uploadDir, "chunks", fileMd5).toString();
}
}
數(shù)據(jù)傳輸對象
@Data
public class CheckResult {
private boolean uploaded;
private List<Integer> uploadedChunks;
}
@Data
public class UploadResult {
private boolean success;
private String message;
}
@Data
public class MergeRequest {
private String fileName;
private String fileMd5;
private Integer totalChunks;
}
@Data
public class MergeResult {
private boolean success;
private String message;
private String filePath;
}
應(yīng)用配置
# application.properties spring.servlet.multipart.max-file-size=10GB spring.servlet.multipart.max-request-size=10GB file.upload-dir=/data/uploads
關(guān)鍵技術(shù)點(diǎn)
- 分片上傳:將大文件分割成小塊,分別上傳
- 斷點(diǎn)續(xù)傳:記錄已上傳的分片,網(wǎng)絡(luò)中斷后可以從中斷處繼續(xù)
- 文件校驗(yàn):通過MD5驗(yàn)證文件完整性
- 進(jìn)度監(jiān)控:實(shí)時(shí)顯示上傳進(jìn)度
- 內(nèi)存優(yōu)化:流式處理,避免內(nèi)存溢出
優(yōu)化建議
- 增加重試機(jī)制:網(wǎng)絡(luò)異常時(shí)自動(dòng)重試
- 并行上傳:同時(shí)上傳多個(gè)分片提高速度
- 壓縮傳輸:對分片進(jìn)行壓縮減少網(wǎng)絡(luò)傳輸量
- 安全驗(yàn)證:添加身份驗(yàn)證和文件類型檢查
- 分布式存儲(chǔ):支持分布式文件系統(tǒng)存儲(chǔ)
這種方案可以有效解決大文件上傳的各種問題,提供穩(wěn)定可靠的上傳體驗(yàn)。
到此這篇關(guān)于Java大文件上傳(分片上傳+斷點(diǎn)續(xù)傳)的解決方案詳解的文章就介紹到這了,更多相關(guān)Java大文件上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java面向?qū)ο蠡A(chǔ)知識(shí)之委托和lambda
這篇文章主要介紹了Java面向?qū)ο蟮闹泻?lambda,文中有非常詳細(xì)的代碼示例,對正在學(xué)習(xí)java基礎(chǔ)的小伙伴們有很好的幫助,需要的朋友可以參考下2021-11-11
java實(shí)現(xiàn)圖片轉(zhuǎn)base64字符串 java實(shí)現(xiàn)base64字符串轉(zhuǎn)圖片
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)圖片轉(zhuǎn)base64字符串,java實(shí)現(xiàn)base64字符串轉(zhuǎn)圖片,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-02-02
解決ThingsBoard編譯報(bào)錯(cuò)問題:Failure?to?find?org.gradle:gradle-too
這篇文章主要介紹了ThingsBoard編譯報(bào)錯(cuò):Failure?to?find?org.gradle:gradle-tooling-api:jar:6.3,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03
Spring中任務(wù)調(diào)度之解讀@Scheduled和@Schedules注解的使用
這篇文章主要介紹了Spring中任務(wù)調(diào)度之解讀@Scheduled和@Schedules注解的使用,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-04-04
Java使用ThreadLocal實(shí)現(xiàn)當(dāng)前登錄信息的存取功能
ThreadLocal和其他并發(fā)工具一樣,也是用于解決多線程并發(fā)訪問,下這篇文章主要給大家介紹了關(guān)于Java使用ThreadLocal實(shí)現(xiàn)當(dāng)前登錄信息的存取功能,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02

