Java大文件上传(分片上传+断点续传)的解决方案详解

 更新时间:2025年12月16日 09:44:24   作者:悟空码字  
大文件上传通常指上传超过几百MB甚至几个GB的文件,与普通文件上传相比,大文件上传会面临很多调整,下面小编就和大家详细讲讲具体的解决方案吧

什么是大文件上传

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

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

解决方案:分片上传

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

前端代码示例 (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)

配置文件上传设置

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

数据传输对象

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

应用配置

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

关键技术点

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

优化建议

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

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

到此这篇关于Java大文件上传(分片上传+断点续传)的解决方案详解的文章就介绍到这了,更多相关Java大文件上传内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java Split 实现去除一个空格和多个空格

    java Split 实现去除一个空格和多个空格

    这篇文章主要介绍了java Split 实现去除一个空格和多个空格,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-10-10
  • Java面向对象编程中final关键字的使用方法详解

    Java面向对象编程中final关键字的使用方法详解

    这篇文章主要介绍了Java面向对象编程中final关键字的使用方法详解,包括对内部匿名类无法访问外面的非 final 的变量问题的解读,需要的朋友可以参考下
    2016-06-06
  • springboot使用GuavaCache做简单缓存处理的方法

    springboot使用GuavaCache做简单缓存处理的方法

    这篇文章主要介绍了springboot使用GuavaCache做简单缓存处理的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-01-01
  • Spring Boot配置元数据方法教程

    Spring Boot配置元数据方法教程

    这篇文章主要介绍了Spring Boot配置元数据方法教程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-12-12
  • MyBatis-plus使用lambda条件构造器报错问题及解决

    MyBatis-plus使用lambda条件构造器报错问题及解决

    这篇文章主要介绍了MyBatis-plus使用lambda条件构造器报错问题及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • SpringBoot集成MQTT实现交互服务通信

    SpringBoot集成MQTT实现交互服务通信

    MQTT非常适用于物联网领域,本文主要介绍了SpringBoot集成MQTT实现交互服务通信,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-08-08
  • Java高效实现TXT转Word的完整指南

    Java高效实现TXT转Word的完整指南

    在日常文档处理工作中,文件格式转换是一个常见且必要的任务,本文将详细介绍如何在 Java 环境下,将 TXT 文件高效地转换为 Word 文件,并演示如何为文本设置基本格式,希望对大家有所帮助
    2025-12-12
  • 实战分布式医疗挂号系统之设置微服务接口开发模块

    实战分布式医疗挂号系统之设置微服务接口开发模块

    这篇文章主要为大家介绍了实战分布式医疗挂号系统之接口开发医院设置微服务模块,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-04-04
  • MyBatis-Plus通过插件将数据库表生成Entiry,Mapper.xml,Mapper.class的方式

    MyBatis-Plus通过插件将数据库表生成Entiry,Mapper.xml,Mapper.class的方式

    今天小编就为大家分享一篇关于MyBatis-Plus通过插件将数据库表生成Entiry,Mapper.xml,Mapper.class的方式,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-02-02
  • SpringBoot结合JWT实现用户登录、注册、鉴权

    SpringBoot结合JWT实现用户登录、注册、鉴权

    用户登录、注册及鉴权是我们基本所有系统必备的,也是很核心重要的一块,本文主要介绍了SpringBoot结合JWT实现用户登录、注册、鉴权,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2023-05-05

最新评论