Java Spring Boot断点续传(上传/下载)代码实现及相关库咨询
Java Spring Boot 断点续传实现与工具库推荐
一、断点续传上传实现
核心逻辑为分片上传+已上传分片校验:客户端将文件分割为固定大小的分片,携带文件唯一标识、分片序号等参数上传;服务端先校验已上传分片,避免重复传输,所有分片上传完成后合并为完整文件。
代码实现
Controller层
import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.util.Map; @RestController @RequestMapping("/file") public class FileUploadController { @Resource private FileUploadService uploadService; // 校验已上传分片 @GetMapping("/check-chunks") public Map<String, Object> checkChunks(@RequestParam String fileHash) { return uploadService.checkUploadedChunks(fileHash); } // 上传单个分片 @PostMapping("/upload-chunk") public String uploadChunk(@RequestParam String fileHash, @RequestParam Integer chunkIndex, @RequestParam Integer totalChunks, @RequestParam MultipartFile chunk) { uploadService.saveChunk(fileHash, chunkIndex, chunk); // 校验是否所有分片上传完成,完成则合并 if (uploadService.isAllChunksUploaded(fileHash, totalChunks)) { uploadService.mergeChunks(fileHash); return "文件上传完成"; } return "分片上传成功"; } }
Service层
import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @Service public class FileUploadService { // 临时分片存储目录 private static final String TEMP_DIR = "./temp-chunks/"; // 最终文件存储目录 private static final String UPLOAD_DIR = "./uploaded-files/"; public FileUploadService() { // 初始化存储目录 createDirIfNotExist(TEMP_DIR); createDirIfNotExist(UPLOAD_DIR); } private void createDirIfNotExist(String dirPath) { Path path = Paths.get(dirPath); if (!Files.exists(path)) { try { Files.createDirectories(path); } catch (IOException e) { throw new RuntimeException("创建存储目录失败", e); } } } // 校验已上传分片 public Map<String, Object> checkUploadedChunks(String fileHash) { Map<String, Object> result = new HashMap<>(); Path chunkDir = Paths.get(TEMP_DIR + fileHash); if (Files.exists(chunkDir)) { try { Set<Integer> uploadedChunks = Files.list(chunkDir) .map(path -> path.getFileName().toString()) .map(name -> Integer.parseInt(name.split("-")[1])) .collect(Collectors.toSet()); result.put("uploadedChunks", uploadedChunks); result.put("exists", true); } catch (IOException e) { throw new RuntimeException("校验分片状态失败", e); } } else { result.put("exists", false); } return result; } // 保存单个分片 public void saveChunk(String fileHash, Integer chunkIndex, MultipartFile chunk) { Path chunkDir = Paths.get(TEMP_DIR + fileHash); createDirIfNotExist(chunkDir.toString()); Path chunkPath = chunkDir.resolve("chunk-" + chunkIndex); try (InputStream is = chunk.getInputStream(); OutputStream os = new FileOutputStream(chunkPath.toFile())) { byte[] buffer = new byte[1024 * 1024]; int len; while ((len = is.read(buffer)) != -1) { os.write(buffer, 0, len); } } catch (IOException e) { throw new RuntimeException("保存分片失败", e); } } // 判断是否所有分片已上传 public boolean isAllChunksUploaded(String fileHash, Integer totalChunks) { Path chunkDir = Paths.get(TEMP_DIR + fileHash); if (!Files.exists(chunkDir)) { return false; } try { long chunkCount = Files.list(chunkDir).count(); return chunkCount == totalChunks; } catch (IOException e) { throw new RuntimeException("统计分片数量失败", e); } } // 合并所有分片为完整文件 public void mergeChunks(String fileHash) { // 实际项目中需关联原文件名,此处简化为用fileHash命名 String fileName = fileHash + ".dat"; Path targetFile = Paths.get(UPLOAD_DIR + fileName); try (OutputStream os = new FileOutputStream(targetFile.toFile())) { Path chunkDir = Paths.get(TEMP_DIR + fileHash); // 按分片序号排序合并 Files.list(chunkDir) .sorted((p1, p2) -> { int idx1 = Integer.parseInt(p1.getFileName().toString().split("-")[1]); int idx2 = Integer.parseInt(p2.getFileName().toString().split("-")[1]); return idx1 - idx2; }) .forEach(chunkPath -> { try (InputStream is = new FileInputStream(chunkPath.toFile())) { byte[] buffer = new byte[1024 * 1024]; int len; while ((len = is.read(buffer)) != -1) { os.write(buffer, 0, len); } } catch (IOException e) { throw new RuntimeException("合并分片失败", e); } }); // 合并完成后删除临时分片目录 deleteDir(chunkDir); } catch (IOException e) { throw new RuntimeException("合并文件失败", e); } } private void deleteDir(Path dirPath) { try { Files.walk(dirPath) .sorted(java.util.Comparator.reverseOrder()) .map(Path::toFile) .forEach(File::delete); } catch (IOException e) { throw new RuntimeException("删除临时目录失败", e); } } }
二、断点续传下载实现
基于HTTP协议的Range请求头实现:客户端请求时携带文件下载区间,服务端解析后返回对应区间的文件内容,返回206 Partial Content状态码。
代码实现
Controller层
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; @RestController @RequestMapping("/file") public class FileDownloadController { @Resource private FileDownloadService downloadService; @GetMapping("/download/{fileName}") public void downloadFile(@PathVariable String fileName, @RequestHeader(value = "Range", required = false) String rangeHeader, HttpServletResponse response) throws Exception { downloadService.handleRangeDownload(fileName, rangeHeader, response); } }
Service层
import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.RandomAccessFile; import java.nio.charset.StandardCharsets; @Service public class FileDownloadService { private static final String UPLOAD_DIR = "./uploaded-files/"; public void handleRangeDownload(String fileName, String rangeHeader, HttpServletResponse response) throws Exception { File file = new File(UPLOAD_DIR + fileName); if (!file.exists()) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } long fileLength = file.length(); long start = 0; long end = fileLength - 1; // 解析Range请求头 if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { String[] rangeParts = rangeHeader.substring(6).split("-"); start = Long.parseLong(rangeParts[0]); if (rangeParts.length > 1 && !rangeParts[1].isEmpty()) { end = Long.parseLong(rangeParts[1]); } // 修正超出文件范围的end值 end = Math.min(end, fileLength - 1); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength); } else { response.setStatus(HttpServletResponse.SC_OK); } // 设置响应头 response.setHeader("Accept-Ranges", "bytes"); response.setHeader("Content-Length", String.valueOf(end - start + 1)); response.setHeader("Content-Type", "application/octet-stream"); response.setHeader("Content-Disposition", "attachment; filename=\"" + new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1) + "\""); // 读取文件对应区间并写入响应流 try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { raf.seek(start); byte[] buffer = new byte[1024 * 1024]; long remaining = end - start + 1; int len; while (remaining > 0 && (len = raf.read(buffer, 0, (int) Math.min(buffer.length, remaining))) != -1) { response.getOutputStream().write(buffer, 0, len); remaining -= len; } response.getOutputStream().flush(); } } }
三、Java生态中断点续传工具库
- Apache Commons FileUpload:基础文件上传工具,可基于其API扩展实现分片上传,提供稳定的文件流处理能力。
- Spring Content:Spring生态内容管理框架,原生支持分片上传/断点续传,无缝集成Spring Boot,支持本地、S3等多种存储后端。
- FastDFS Java Client:适配FastDFS分布式文件系统,内置断点续传能力,适合分布式大文件存储场景。
- Aliyun OSS SDK for Java:阿里云OSS官方SDK,原生支持断点续传上传和下载,适合采用云存储的项目。
- MinIO Java SDK:开源对象存储MinIO的官方SDK,兼容S3 API,支持分片上传与断点续传,适合自建存储服务的场景。
内容的提问来源于stack exchange,提问作者Ajith S P
相关产品推荐
相关产品推荐

