Java大文件上传解决方案:分片上传+断点续传实战

117 阅读4分钟

大家好,我是小悟。

  • 什么是大文件上传

    大文件上传通常指上传超过几百MB甚至几个GB的文件。与普通文件上传相比,大文件上传面临以下挑战:

    1. 内存限制 - 一次性加载整个文件到内存会导致内存溢出
    2. 网络稳定性 - 上传过程中网络中断需要能够断点续传
    3. 超时问题 - 长时间上传可能导致连接超时
    4. 进度监控 - 需要实时显示上传进度
    5. 文件校验 - 确保文件完整性和安全性

    解决方案:分片上传

    大文件上传的核心思想是将文件分割成多个小块,分别上传,最后在服务器端合并。

    前端代码示例 (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 = `上传进度: ${percentage}%`;
            }
    
            // 计算文件MD5(简化版,实际应使用更可靠的库)
            async function calculateFileMD5(file) {
                // 这里使用简单的文件名+大小模拟MD5
                // 实际项目中应使用 spark-md5 等库
                return btoa(file.name + file.size).replace(/[^a-zA-Z0-9]/g, '');
            }
        </script>
    </body>
    </html>
    

    后端Java代码示例 (Spring Boot)

    1. 配置文件上传设置

    @Configuration
    public class UploadConfig {
        
        @Bean
        public MultipartConfigElement multipartConfigElement() {
            MultipartConfigFactory factory = new MultipartConfigFactory();
            factory.setMaxFileSize("10GB");
            factory.setMaxRequestSize("10GB");
            return factory.createMultipartConfig();
        }
    }
    

    2. 文件上传控制器

    @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 {
                // 创建分片目录
                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();
                
                // 创建目标文件
                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();
        }
    }
    

    3. 数据传输对象

    @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;
    }
    

    4. 应用配置

    # application.properties
    spring.servlet.multipart.max-file-size=10GB
    spring.servlet.multipart.max-request-size=10GB
    file.upload-dir=/data/uploads
    

    关键技术点

    1. 分片上传:将大文件分割成小块,分别上传
    2. 断点续传:记录已上传的分片,网络中断后可以从中断处继续
    3. 文件校验:通过MD5验证文件完整性
    4. 进度监控:实时显示上传进度
    5. 内存优化:流式处理,避免内存溢出

    优化建议

    1. 增加重试机制:网络异常时自动重试
    2. 并行上传:同时上传多个分片提高速度
    3. 压缩传输:对分片进行压缩减少网络传输量
    4. 安全验证:添加身份验证和文件类型检查
    5. 分布式存储:支持分布式文件系统存储

    这种方案可以有效解决大文件上传的各种问题,提供稳定可靠的上传体验。

Java大文件上传解决方案:分片上传+断点续传实战.png

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。

您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海