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

简介: 大文件上传通常指上传超过几百MB甚至几个GB的文件。与普通文件上传相比,大文件上传面临以下挑战:一次性加载整个文件到内存会导致内存溢出,上传过程中网络中断需要能够断点续传。

大家好,我是小悟。

  • 什么是大文件上传

大文件上传通常指上传超过几百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. 分布式存储:支持分布式文件系统存储

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

image.png

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


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

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

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

相关文章
|
3月前
|
存储 监控 前端开发
大文件上传下载处理方案-断点续传,秒传,分片,合并
本文介绍了大文件上传下载的断点续传技术方案。上传方面,通过前端将大文件分块(如5MB/块),后端使用MinIO存储分块并合并,实现断点续传和秒传功能。下载方面,采用Range请求分片下载,前端合并分片触发下载。技术要点包括:1)前端分块计算MD5;2)后端MinIO存储管理;3)分片校验与合并;4)进度监控和异常处理。该方案解决了大文件传输中断问题,提升用户体验,适用于视频等大文件传输场景,完整代码示例包含前后端实现。
|
JavaScript 前端开发 Java
springboot整合minio+vue实现大文件分片上传,断点续传(复制可用,包含minio工具类)
springboot整合minio+vue实现大文件分片上传,断点续传(复制可用,包含minio工具类)
4448 2
|
前端开发 JavaScript Java
RSA加密---前端---后端解密
RSA加密---前端加---后端解密
2447 0
|
2月前
|
消息中间件 存储 Cloud Native
【消息队列MQ】主流消息队列MQ全方位对比:Kafka、RocketMQ、RabbitMQ、Pulsar
本文系统梳理Kafka、RocketMQ、RabbitMQ、Pulsar四大主流MQ的核心定位、架构差异、性能特性、运维生态及精准选型逻辑,覆盖从金融级可靠、高吞吐流处理到云原生多租户等全场景,助你构建结构化MQ知识体系,实现科学决策。
|
6月前
|
人工智能 NoSQL 前端开发
Chap03. SpringAI
SpringAI整合多款主流大模型,支持对话、函数调用与RAG等架构,提供统一API简化开发。通过ChatClient封装交互,结合Prompt工程、工具调用与知识检索,可快速构建智能客服、哄哄模拟器、ChatPDF等应用,并支持多模态与持久化扩展,助力AI应用高效落地。
|
11月前
|
消息中间件 缓存 监控
MQ消息积压 / Rocketmq 积压 最全的处理方案。 (秒懂+图解+史上最全)
MQ消息积压 / Rocketmq 积压 最全的处理方案。 (秒懂+图解+史上最全)
MQ消息积压 / Rocketmq 积压 最全的处理方案。 (秒懂+图解+史上最全)
|
5月前
|
消息中间件 NoSQL Java
库存保卫战:电商系统防超卖的5把利刃与Java实战
在电商系统中,超卖是指商品库存不足以满足所有购买请求时,系统仍然接受了超过库存数量的订单。这会导致:商家无法正常发货,用户体验受损,平台信誉下降。
191 0
|
网络协议 安全 Java
实现Java语言的文件断点续传功能的技术方案。
像这样,我们就完成了一项看似高科技、实则亲民的小工程。这样的技术实现不仅具备实用性,也能在面对网络不稳定的挑战时,稳稳地、不失乐趣地完成工作。
600 0
|
前端开发 NoSQL Redis
如何实现大文件上传:秒传、断点续传、分片上传
如何实现大文件上传:秒传、断点续传、分片上传
1472 0
|
缓存 监控 负载均衡
将近2万字的Dubbo原理解析,彻底搞懂dubbo
市面上有很多基于RPC思想实现的框架,比如有Dubbo。今天就从Dubbo的SPI机制、服务注册与发现源码及网络通信过程去深入剖析下Dubbo。
30821 9

热门文章

最新文章