You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 02:40:18