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

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


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

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

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

相关文章
|
23天前
|
存储 监控 Java
Java实现接口幂等性:程序员的“后悔药”
接口幂等性就像是给系统穿了件"防重复甲",让它在面对:用户疯狂点击、网络抽风重试、系统自动重试等这些情况时,都能淡定地说:"老弟,这个请求我已经处理过了,结果在这,拿去吧!"
155 4
|
应用服务中间件 Apache
springmvc中报错Request processing failed;
springmvc中报错Request processing failed;
|
资源调度
npm i时报错npm ERR! code ERESOLVE npm ERR! ERESOLVE could not resolve npm ERR! npm ERR! While resolving
npm i时报错npm ERR! code ERESOLVE npm ERR! ERESOLVE could not resolve npm ERR! npm ERR! While resolving
891 0
|
缓存
RestTemplate请求访问简单使用
RestTemplate请求访问简单使用
359 1
|
Nacos 微服务
Nacos与Eureka的区别
Eureka和Nacos均支持服务注册发现、基于心跳的健康检查及AP模式下的集群数据同步。主要区别在于:心跳频率、服务剔除机制、服务检测与清理周期不同,Nacos还额外提供配置管理功能。
739 0
|
存储 Linux 虚拟化
Hyper-V 安装 CentOS 8.5
本文档介绍了在 Windows 10 上使用 Hyper-V 安装 CentOS 8.5.2111 的详细步骤
1131 3
|
存储 小程序 API
|
负载均衡 Cloud Native 数据可视化
Nacos与Eureka比较?
【6月更文挑战第29天】Nacos与Eureka比较?
792 2
|
JSON API 数据格式
如何使用Flask request对象处理请求
在 Flask 中,request对象是处理 HTTP 请求的重要工具之一。它提供了许多属性和方法,可以帮助我们获取请求的相关信息和数据。本文将向你介绍request对象的常用方法以及如何在 Flask 应用程序中使用它。
460 3
|
缓存 安全 Java
Shiro框架的知识点一网打尽,生命不息,学习不止
Shiro框架的知识点一网打尽,生命不息,学习不止
339 0

热门文章

最新文章