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

举报
悟空码字 发表于 2026/02/12 11:19:08 2026/02/12
【摘要】 大文件上传通常指上传超过几百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. 分布式存储:支持分布式文件系统存储

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

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

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

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

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

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。