阅读量:0
一、项目搭建
创建 Spring Boot 项目: 创建一个新的 Spring Boot 项目,添加 Web 依赖。
添加依赖: 在
pom.xml
文件中添加以下依赖:
<dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency>
二、后端实现
配置 MultipartResolver: 在 Spring Boot 配置类中添加以下代码:
@Configuration public class MyWebAppConfigurer implements WebMvcConfigurer { @Bean public MultipartResolver multipartResolver() { CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(); multipartResolver.setMaxUploadSize(-1); // 设置最大上传大小,-1 表示无限制 return multipartResolver; } }
创建 FileUploadService: 创建一个服务类,用于处理文件上传逻辑:
@Service public class FileUploadService { private String uploadDir = "upload/"; // 设置上传目录 public String initUpload(String fileName, long fileSize, int chunkSize) { // 1. 生成任务 ID (UUID) String fileId = UUID.randomUUID().toString();//这里根据实际情况考虑到断点续传的功能,同一个文件生成的标识要一样,后期可以根据这个判断文件上传进度 // 2. 创建临时目录: uploadDir/fileId File dir = new File(uploadDir, fileId); if (!dir.exists()) { dir.mkdirs(); } // 3. 返回 fileId return fileId; } public String uploadChunk(String fileId, int chunkIndex, int totalChunks, MultipartFile file) throws IOException { String fileUrl=""; // 1. 获取分片文件 String fileName = file.getOriginalFilename(); // 2. 保存分片到临时目录: uploadDir/fileId/chunkIndex File chunkFile = new File(uploadDir, fileId + "/" + chunkIndex); file.transferTo(chunkFile); // 检查所有分片是否已上传完成 if (allChunksUploaded(fileId, totalChunks)) { String fileName=datePath()+"/"+fileId;//这里可以不用文件名,如果需要用,则要在controller上传请求增加一个fileName参数 // 合并分片 fileUrl = mergeChunks(fileId, fileName); } // 3. 校验分片 MD5 (可选) return fileUrl; } //判断所有的分片是否都上传完毕 private boolean allChunksUploaded(String fileId, int totalChunks) { for (int i = 0; i < totalChunks; i++) { File chunkFile = new File(uploadDir + fileName + ".chunk" + i); if (!chunkFile.exists()) { return false; } } return true; } public String mergeChunks(String fileId, String fileName) throws IOException { // 1. 获取所有分片文件 File dir = new File(uploadDir, fileId); File[] chunkFiles = dir.listFiles(); // 2. 按顺序合并分片 File mergedFile = new File(uploadDir, fileName); try (FileOutputStream fos = new FileOutputStream(mergedFile, true)) { for (File chunkFile : chunkFiles) { try (FileInputStream fis = new FileInputStream(chunkFile)) { IOUtils.copy(fis, fos); } } } // 3. 删除临时目录 FileUtils.deleteDirectory(dir); // 4. 校验文件 MD5 (可选) // 5. 返回文件存储路径 return uploadDir + fileName; } /** * 日期路径 即年/月/日 如2018/08/08 */ public static final String datePath() { Date now = new Date(); return DateFormatUtils.format(now, "yyyy/MM/dd"); } }
创建 FileUploadController: 创建一个控制器类,用于处理文件上传请求:
要在分片上传的基础上实现断点续传,需要在服务端记录每个文件的上传进度,并在客户端请求上传时返回已上传的分片信息。
- 使用数据库或其他存储机制记录每个文件的上传进度。
- 可以使用以下信息标识一个上传任务:
- fileId: 全局唯一标识符,例如 UUID
- 同一个文件的标识是一样的,这样保证接这上次的进度继续上传。
@RestController public class FileUploadController { @Autowired private FileUploadService fileUploadService; private final ExecutorService executorService = Executors.newFixedThreadPool(5); // 线程池大小可配置 private final Map<String, Set<Integer>> uploadProgress = new HashMap<>(); // 使用内存存储上传进度,实际应用中建议使用数据库 @PostMapping("/upload/init") public ResponseEntity<String> initUpload(@RequestParam("fileName") String fileName, @RequestParam("fileSize") long fileSize, @RequestParam("chunkSize") int chunkSize) { String fileId = fileUploadService.initUpload(fileName, fileSize, chunkSize); return ResponseEntity.ok(fileId); } @PostMapping("/upload/chunk") public ResponseEntity<Void> uploadChunk(@RequestParam("fileId") String fileId, @RequestParam("chunkIndex") int chunkIndex, @RequestParam("totalChunks") int totalChunks, @RequestParam("file") MultipartFile file) throws IOException { String filePath = ""; try { // 获取或创建上传进度记录 Set<Integer> uploadedChunks = uploadProgress.computeIfAbsent(fileId, k -> new HashSet<>()); // 如果分片已上传,则跳过 if (uploadedChunks.contains(chunkIndex)) { return ResponseEntity.ok(new UploadResponse(uploadedChunks)); } // 使用线程池处理每个分片上传 executorService.execute(() -> { try { filePath = fileUploadService.uploadChunk(fileId, chunkIndex, totalChunks, file); } catch (IOException e) { // 处理异常,例如记录日志或返回错误信息 e.printStackTrace(); } }); return ResponseEntity.status(HttpStatus.ACCEPTED).body(new UploadResponse(uploadedChunks, filePath)); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error uploading chunk."); } //return ResponseEntity.ok().build(); } } // 用于响应上传请求,携带已上传分片信息 class UploadResponse { Set<Integer> uploadedChunks; String filePath; public UploadResponse(Set<Integer> uploadedChunks, String filePath) { this.uploadedChunks = uploadedChunks; this.filePath = filePath; } public UploadResponse(Set<Integer> uploadedChunks) { this.uploadedChunks = uploadedChunks; } // ... getter setter 方法 ... }
三、前端实现
HTML 页面: 创建一个简单的 HTML 页面,包含文件选择按钮、上传进度条和相关信息展示区域。
JavaScript 代码: 使用 JavaScript 实现文件分割、分片上传、合并请求和上传进度展示等功能。
// 选择文件 const fileInput = document.getElementById('fileInput'); fileInput.addEventListener('change', (event) => { const file = event.target.files[0]; // ... 文件分割、上传逻辑 }); // 文件分割 const chunkSize = 4 * 1024 * 1024; // 4MB const chunks = sliceFile(file, chunkSize); const totalChunks = Math.ceil(file.size / chunkSize); // 初始化上传 const fileId = await initUpload(file.name, file.size, chunkSize); // 并发上传分片 const uploadPromises = chunks.map((chunk, index) => uploadChunk(fileId, index, totalChunks, chunk) ); // 上传完成合并得到文件url await Promise.all(uploadPromises); async function uploadChunk(fileId, chunkIndex, totalChunks, chunk) { // ... 创建 FormData ... const formData = new FormData(); formData.append('file', chunk); formData.append('chunkIndex', chunkIndex); formData.append('totalChunks', totalChunks); formData.append('fileId', fileId); // 添加 fileId 参数 const response = await fetch('/upload', { method: 'POST', body: formData, }); //...处理响应数据 } function sliceFile(file, chunkSize) { let chunks = []; let count = Math.ceil(file.size / chunkSize); for (let i = 0; i < count; i++) { let offset = i * chunkSize; let chunk = file.slice(offset, offset + chunkSize + 1); chunks.push(chunk); } return chunks; }
四、上传进度展示
后端: 在
FileUploadService
中添加方法,根据fileId
返回已上传分片数量或计算上传进度百分比。前端: 使用
setInterval
定时请求后端获取上传进度,并更新进度条。
前端html代码使用示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>文件上传</title> </head> <body> <h1>大文件分片上传</h1> <input type="file" id="fileInput"> <button id="uploadBtn">上传</button> <div>上传进度:<progress id="progressBar" value="0" max="100"></progress> <span id="progressText">0%</span></div> <script> const fileInput = document.getElementById('fileInput'); const uploadBtn = document.getElementById('uploadBtn'); const progressBar = document.getElementById('progressBar'); const progressText = document.getElementById('progressText'); uploadBtn.addEventListener('click', uploadFile); async function uploadFile() { const file = fileInput.files[0]; if (!file) { alert('请选择文件'); return; } const chunkSize = 4 * 1024 * 1024; // 4MB const fileId = await initUpload(file.name, file.size, chunkSize); const chunks = sliceFile(file, chunkSize); let uploadedChunks = 0; const uploadPromises = chunks.map((chunk, index) => { return uploadChunk(fileId, index, chunks.length, chunk) .then(() => { uploadedChunks++; updateProgress(uploadedChunks / chunks.length); }); }); await Promise.all(uploadPromises); await mergeChunks(fileId); alert('上传完成!'); } function sliceFile(file, chunkSize) { const chunks = []; let offset = 0; while (offset < file.size) { chunks.push(file.slice(offset, offset + chunkSize)); offset += chunkSize; } return chunks; } async function initUpload(fileName, fileSize, chunkSize) { const response = await fetch('/upload/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileName, fileSize, chunkSize }) }); return await response.text(); } async function uploadChunk(fileId, chunkIndex, totalChunks, chunk) { const formData = new FormData(); formData.append('fileId', fileId); formData.append('chunkIndex', chunkIndex); formData.append('totalChunks', totalChunks); formData.append('file', chunk); await fetch('/upload/chunk', { method: 'POST', body: formData }); } async function mergeChunks(fileId) { await fetch(`/upload/merge?fileId=${fileId}`); } function updateProgress(progress) { progressBar.value = progress * 100; progressText.textContent = Math.round(progress * 100) + '%'; } </script> </body> </html>
五、存储上传进度
使用 Redis 存储上传进度
Redis 数据结构:
- 使用 Hash 结构存储每个文件的上传进度,key 为 fileId和chunkIndex,chunkIndex 为分片索引,value 为 true 或 false,表示分片是否已上传。
代码实现:
- 注入
RedisTemplate
:
@Autowired private RedisTemplate<String, String> redisTemplate;
- 修改
uploadChunk
方法:
private void uploadChunk(String fileId, int chunkIndex, int totalChunks, MultipartFile file) throws IOException { // ... 保存分片文件 ... // 更新上传进度到 Redis String chunkKey = fileId + ":" + chunkIndex; redisTemplate.opsForValue().set(chunkKey, "true"); // 检查所有分片是否已上传完成 if (redisTemplate.opsForHash().size(fileId) == totalChunks) { // ... 合并分片 ... // 清除上传进度 redisTemplate.delete(fileId); } }
- 添加
/upload/progress
接口:
@GetMapping("/upload/progress") public ResponseEntity<UploadResponse> getUploadProgress( @RequestParam("identifier") String identifier ) { Set<String> uploadedChunks = redisTemplate.keys(identifier + ":*"); Set<Integer> uploadedChunkIndices = uploadedChunks.stream() .map(s -> Integer.parseInt(s.substring((identifier + ":").length()))) .collect(Collectors.toSet()); return ResponseEntity.ok(new UploadResponse(uploadedChunkIndices)); }
- 前端js使用
// ... 其他代码 ... // 选择文件 const fileInput = document.getElementById('fileInput'); fileInput.addEventListener('change', (event) => { const file = event.target.files[0]; // ... 文件分割、上传逻辑 }); // 文件分割 const chunkSize = 4 * 1024 * 1024; // 4MB const chunks = sliceFile(file, chunkSize); const totalChunks = Math.ceil(file.size / chunkSize); // 初始化上传 const fileId = await initUpload(file.name, file.size, chunkSize); async function uploadFile(chunks) { // 获取已上传的分片信息 const uploadedChunks = await getUploadedChunks(fileId, file.name); // ... 根据 uploadedChunks 调整分片上传逻辑 ... if(...){//获取uploadedChunks为分片的索引,表示当前文件上传的进度,根据uploadedChunks的具体数据类型去取 for (let i = (uploadedChunks); i < chunks.length; i++) { //const start = i * chunkSize; //const end = Math.min(start + chunkSize, file.size); //const chunk = file.slice(start, end); const chunk = chunks[i]; await uploadChunk(fileId, i, totalChunks, chunk); } }else{ // 并发上传分片 chunks.map((chunk, index) => uploadChunk(fileId, index, totalChunks, chunk) ); } } async function getUploadedChunks(fileId, fileName) { const response = await fetch(`/upload/progress?identifier=${fileId}&fileName=${fileName}`); const data = await response.json(); return data.uploadedChunks || []; } async function uploadChunk(fileId, chunkIndex, totalChunks, chunk) { // ... 创建 FormData ... const formData = new FormData(); formData.append('file', chunk); formData.append('chunkIndex', chunkIndex); formData.append('totalChunks', totalChunks); formData.append('fileId', fileId); // 添加 fileId 参数 const response = await fetch('/upload', { method: 'POST', body: formData, }); //...处理响应数据 } function sliceFile(file, chunkSize) { let chunks = []; let count = Math.ceil(file.size / chunkSize); for (let i = 0; i < count; i++) { let offset = i * chunkSize; let chunk = file.slice(offset, offset + chunkSize + 1); chunks.push(chunk); } return chunks; }
重新上传获取进度:
- 用户重新选择同一个文件上传时,需要生成相同的 f
ileId
。 - 在前端上传前,调用
/upload/progress
接口,传入 fileId 获取已上传的分片信息。 - 根据返回的已上传分片信息,跳过已上传的分片,继续上传剩余分片。
六、注意事项
- 以上代码示例省略了部分细节,例如异常处理、MD5 校验等,请根据实际情况进行完善。
- 前端代码需要根据您使用的 JavaScript 框架进行调整。
- 建议您先学习 Spring Boot 文件上传、JavaScript 文件操作和 AJAX 等前端相关知识。
希望这些更详细的步骤和代码片段能够帮助您更好地理解和实现 Spring Boot 断点续传、多线程分片上传功能! 如果您还有其他问题,请随时提出。