文件上传——springboot大文件分片多线程上传功能,前端显示弹出上传进度框
创始人
2024-11-15 04:33:05
0

 一、项目搭建

  1. 创建 Spring Boot 项目: 创建一个新的 Spring Boot 项目,添加 Web 依赖。

  2. 添加依赖: 在 pom.xml 文件中添加以下依赖:

     commons-fileupload     commons-fileupload     1.4       commons-io     commons-io     2.11.0  

二、后端实现

  1. 配置 MultipartResolver: 在 Spring Boot 配置类中添加以下代码:

@Configuration public class MyWebAppConfigurer implements WebMvcConfigurer {      @Bean     public MultipartResolver multipartResolver() {         CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();         multipartResolver.setMaxUploadSize(-1); // 设置最大上传大小,-1 表示无限制         return multipartResolver;     } } 
  1. 创建 FileUploadService: 创建一个服务类,用于处理文件上传逻辑:

@Service public class FileUploadService {      private String uploadDir = "upload/"; // 设置上传目录      public String initUpload(String fileName, long fileSize, int chunkSize) {         // 1. 生成任务 ID (UUID)         String fileId = UUID.randomUUID().toString();//这里根据实际情况考虑到断点续传的功能,同一个文件生成的标识要一样,后期可以根据这个判断文件上传进度         // 2. 创建临时目录: uploadDir/fileId         File dir = new File(uploadDir, fileId);         if (!dir.exists()) {             dir.mkdirs();         }         // 3. 返回 fileId         return fileId;     }      public String uploadChunk(String fileId, int chunkIndex, int totalChunks, MultipartFile file) throws IOException {         String fileUrl="";         // 1. 获取分片文件         String fileName = file.getOriginalFilename();         // 2. 保存分片到临时目录: uploadDir/fileId/chunkIndex         File chunkFile = new File(uploadDir, fileId + "/" + chunkIndex);         file.transferTo(chunkFile);          // 检查所有分片是否已上传完成         if (allChunksUploaded(fileId, totalChunks)) {             String fileName=datePath()+"/"+fileId;//这里可以不用文件名,如果需要用,则要在controller上传请求增加一个fileName参数             // 合并分片             fileUrl = mergeChunks(fileId, fileName);         }         // 3. 校验分片 MD5 (可选)         return fileUrl;     }      //判断所有的分片是否都上传完毕     private boolean allChunksUploaded(String fileId, int totalChunks) {         for (int i = 0; i < totalChunks; i++) {             File chunkFile = new File(uploadDir + fileName + ".chunk" + i);             if (!chunkFile.exists()) {                 return false;             }         }         return true;     }      public String mergeChunks(String fileId, String fileName) throws IOException {         // 1. 获取所有分片文件         File dir = new File(uploadDir, fileId);         File[] chunkFiles = dir.listFiles();         // 2. 按顺序合并分片         File mergedFile = new File(uploadDir, fileName);         try (FileOutputStream fos = new FileOutputStream(mergedFile, true)) {             for (File chunkFile : chunkFiles) {                 try (FileInputStream fis = new FileInputStream(chunkFile)) {                     IOUtils.copy(fis, fos);                 }             }         }         // 3. 删除临时目录         FileUtils.deleteDirectory(dir);         // 4. 校验文件 MD5 (可选)         // 5. 返回文件存储路径         return uploadDir + fileName;     }      /**      * 日期路径 即年/月/日 如2018/08/08      */     public static final String datePath()     {         Date now = new Date();         return DateFormatUtils.format(now, "yyyy/MM/dd");     } } 
  1. 创建 FileUploadController: 创建一个控制器类,用于处理文件上传请求:

        要在分片上传的基础上实现断点续传,需要在服务端记录每个文件的上传进度,并在客户端请求上传时返回已上传的分片信息。

  • 使用数据库或其他存储机制记录每个文件的上传进度。
  • 可以使用以下信息标识一个上传任务:
    • fileId: 全局唯一标识符,例如 UUID
    • 同一个文件的标识是一样的,这样保证接这上次的进度继续上传。

        

@RestController public class FileUploadController {      @Autowired     private FileUploadService fileUploadService;     private final ExecutorService executorService = Executors.newFixedThreadPool(5); // 线程池大小可配置     private final Map> uploadProgress = new HashMap<>(); // 使用内存存储上传进度,实际应用中建议使用数据库      @PostMapping("/upload/init")     public ResponseEntity initUpload(@RequestParam("fileName") String fileName,                                               @RequestParam("fileSize") long fileSize,                                               @RequestParam("chunkSize") int chunkSize) {         String fileId = fileUploadService.initUpload(fileName, fileSize, chunkSize);         return ResponseEntity.ok(fileId);     }      @PostMapping("/upload/chunk")     public ResponseEntity uploadChunk(@RequestParam("fileId") String fileId,                                              @RequestParam("chunkIndex") int chunkIndex,                                               @RequestParam("totalChunks") int totalChunks,                                              @RequestParam("file") MultipartFile file) throws IOException {         String filePath = "";         try {             // 获取或创建上传进度记录             Set uploadedChunks = uploadProgress.computeIfAbsent(fileId, k -> new HashSet<>());              // 如果分片已上传,则跳过             if (uploadedChunks.contains(chunkIndex)) {                 return ResponseEntity.ok(new UploadResponse(uploadedChunks));             }              // 使用线程池处理每个分片上传             executorService.execute(() -> {                 try {                     filePath = fileUploadService.uploadChunk(fileId, chunkIndex, totalChunks, file);                 } catch (IOException e) {                     // 处理异常,例如记录日志或返回错误信息                     e.printStackTrace();                 }             });              return ResponseEntity.status(HttpStatus.ACCEPTED).body(new UploadResponse(uploadedChunks, filePath));         } catch (Exception e) {             return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error uploading chunk.");         }         //return ResponseEntity.ok().build();     }  }  // 用于响应上传请求,携带已上传分片信息 class UploadResponse {     Set uploadedChunks;     String filePath;      public UploadResponse(Set uploadedChunks, String filePath) {         this.uploadedChunks = uploadedChunks;         this.filePath = filePath;     }      public UploadResponse(Set uploadedChunks) {         this.uploadedChunks = uploadedChunks;     }      // ... getter setter 方法 ... }

三、前端实现

  1. HTML 页面: 创建一个简单的 HTML 页面,包含文件选择按钮、上传进度条和相关信息展示区域。

  2. JavaScript 代码: 使用 JavaScript 实现文件分割、分片上传、合并请求和上传进度展示等功能。

// 选择文件 const fileInput = document.getElementById('fileInput'); fileInput.addEventListener('change', (event) => {     const file = event.target.files[0];     // ... 文件分割、上传逻辑 });  // 文件分割 const chunkSize = 4 * 1024 * 1024; // 4MB const chunks = sliceFile(file, chunkSize);  const totalChunks = Math.ceil(file.size / chunkSize);  // 初始化上传 const fileId = await initUpload(file.name, file.size, chunkSize);  // 并发上传分片 const uploadPromises = chunks.map((chunk, index) =>      uploadChunk(fileId, index, totalChunks, chunk) );  // 上传完成合并得到文件url await Promise.all(uploadPromises);  async function uploadChunk(fileId, chunkIndex, totalChunks, chunk) {   // ... 创建 FormData ...   const formData = new FormData();   formData.append('file', chunk);   formData.append('chunkIndex', chunkIndex);   formData.append('totalChunks', totalChunks);   formData.append('fileId', fileId); // 添加 fileId 参数    const response = await fetch('/upload', {     method: 'POST',     body: formData,   });   //...处理响应数据 }  function sliceFile(file, chunkSize) {   let chunks = [];   let count = Math.ceil(file.size / chunkSize);     for (let i = 0; i < count; i++) {     let offset = i * chunkSize;     let chunk = file.slice(offset, offset + chunkSize + 1);     chunks.push(chunk);   }     return chunks; }

四、上传进度展示

  1. 后端: 在 FileUploadService 中添加方法,根据 fileId 返回已上传分片数量或计算上传进度百分比。

  2. 前端: 使用 setInterval 定时请求后端获取上传进度,并更新进度条。

前端html代码使用示例:

            文件上传   

大文件分片上传

上传进度: 0%

五、存储上传进度

使用 Redis 存储上传进度

  1. Redis 数据结构:

    • 使用 Hash 结构存储每个文件的上传进度,key 为 fileId和chunkIndex,chunkIndex 为分片索引,value 为 true 或 false,表示分片是否已上传。
  2. 代码实现:

  • 注入 RedisTemplate:
@Autowired private RedisTemplate redisTemplate; 
  • 修改 uploadChunk 方法: 
private void uploadChunk(String fileId, int chunkIndex, int totalChunks, MultipartFile file) throws IOException {     // ... 保存分片文件 ...      // 更新上传进度到 Redis     String chunkKey = fileId + ":" + chunkIndex;     redisTemplate.opsForValue().set(chunkKey, "true");      // 检查所有分片是否已上传完成     if (redisTemplate.opsForHash().size(fileId) == totalChunks) {         // ... 合并分片 ...         // 清除上传进度         redisTemplate.delete(fileId);     } } 
  • 添加 /upload/progress 接口: 
@GetMapping("/upload/progress") public ResponseEntity getUploadProgress(         @RequestParam("identifier") String identifier ) {     Set uploadedChunks = redisTemplate.keys(identifier + ":*");     Set uploadedChunkIndices = uploadedChunks.stream()             .map(s -> Integer.parseInt(s.substring((identifier + ":").length())))             .collect(Collectors.toSet());     return ResponseEntity.ok(new UploadResponse(uploadedChunkIndices)); } 
  • 前端js使用
// ... 其他代码 ... // 选择文件 const fileInput = document.getElementById('fileInput'); fileInput.addEventListener('change', (event) => {     const file = event.target.files[0];     // ... 文件分割、上传逻辑 });  // 文件分割 const chunkSize = 4 * 1024 * 1024; // 4MB const chunks = sliceFile(file, chunkSize); const totalChunks = Math.ceil(file.size / chunkSize);  // 初始化上传 const fileId = await initUpload(file.name, file.size, chunkSize);  async function uploadFile(chunks) {   // 获取已上传的分片信息   const uploadedChunks = await getUploadedChunks(fileId, file.name);    // ... 根据 uploadedChunks 调整分片上传逻辑 ...   if(...){//获取uploadedChunks为分片的索引,表示当前文件上传的进度,根据uploadedChunks的具体数据类型去取     for (let i = (uploadedChunks); i < chunks.length; i++) {       //const start = i * chunkSize;       //const end = Math.min(start + chunkSize, file.size);       //const chunk = file.slice(start, end);       const chunk = chunks[i];        await uploadChunk(fileId, i, totalChunks, chunk);     }   }else{     // 并发上传分片     chunks.map((chunk, index) =>          uploadChunk(fileId, index, totalChunks, chunk)     );   }   }  async function getUploadedChunks(fileId, fileName) {   const response = await fetch(`/upload/progress?identifier=${fileId}&fileName=${fileName}`);   const data = await response.json();   return data.uploadedChunks || []; }  async function uploadChunk(fileId, chunkIndex, totalChunks, chunk) {   // ... 创建 FormData ...   const formData = new FormData();   formData.append('file', chunk);   formData.append('chunkIndex', chunkIndex);   formData.append('totalChunks', totalChunks);   formData.append('fileId', fileId); // 添加 fileId 参数    const response = await fetch('/upload', {     method: 'POST',     body: formData,   });   //...处理响应数据 }  function sliceFile(file, chunkSize) {   let chunks = [];   let count = Math.ceil(file.size / chunkSize);     for (let i = 0; i < count; i++) {     let offset = i * chunkSize;     let chunk = file.slice(offset, offset + chunkSize + 1);     chunks.push(chunk);   }     return chunks; }

 重新上传获取进度:

  • 用户重新选择同一个文件上传时,需要生成相同的 fileId
  • 在前端上传前,调用 /upload/progress 接口,传入 fileId 获取已上传的分片信息。
  • 根据返回的已上传分片信息,跳过已上传的分片,继续上传剩余分片。

六、注意事项

  • 以上代码示例省略了部分细节,例如异常处理、MD5 校验等,请根据实际情况进行完善。
  • 前端代码需要根据您使用的 JavaScript 框架进行调整。
  • 建议您先学习 Spring Boot 文件上传、JavaScript 文件操作和 AJAX 等前端相关知识。

希望这些更详细的步骤和代码片段能够帮助您更好地理解和实现 Spring Boot 断点续传、多线程分片上传功能! 如果您还有其他问题,请随时提出。

相关内容

热门资讯

微信炸金花购买房卡/微信斗牛如... 炸金花是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:8488009许多玩家在游戏中会购买房卡来享受...
炸金花微信群购买房卡/牛牛链接... 炸金花是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:33903369许多玩家在游戏中会购买房卡来享...
拼三张从哪里买房卡/新海贝大厅... 拼三张是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:55051770许多玩家在游戏中会购买房卡来享...
微信炸金花房卡一张多少钱/微信... 炸金花是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:8488009许多玩家在游戏中会购买房卡来享受...
微信链接炸金花房卡在哪买的/微... 炸金花是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:33903369许多玩家在游戏中会购买房卡来享...
微信群链接炸金花房卡/微信里斗... 炸金花是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:55051770许多玩家在游戏中会购买房卡来享...
怎么买炸金花房间链接房卡/微信... 炸金花是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:8488009许多玩家在游戏中会购买房卡来享受...
微信玩链接牛牛房卡/新人皇大厅... 斗牛是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:33903369许多玩家在游戏中会购买房卡来享受...
拼三张房卡链接去哪里买/橘子大... 拼三张是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:55051770许多玩家在游戏中会购买房卡来享...
微信玩炸金花怎么买房卡/欢乐游... 炸金花是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:8488009许多玩家在游戏中会购买房卡来享受...
炸金花房卡链接在哪买的/狂飙大... 炸金花是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:33903369许多玩家在游戏中会购买房卡来享...
如何创建牛牛房间卡/牛至尊大厅... 牛牛是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:55051770许多玩家在游戏中会购买房卡来享受...
微信里上玩拼三张购买房卡/神牛... 拼三张是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:8488009许多玩家在游戏中会购买房卡来享受...
微信里面斗牛链接房卡/九酷大厅... 斗牛是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:33903369许多玩家在游戏中会购买房卡来享受...
炸金花如何开好友房间房卡/微信... 炸金花是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:8488009许多玩家在游戏中会购买房卡来享受...
微信炸金花在哪里充值房卡/新天... 炸金花是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:33903369许多玩家在游戏中会购买房卡来享...
微信里面拼三张房卡哪里买/新皇... 拼三张是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:55051770许多玩家在游戏中会购买房卡来享...
微信群开牛牛房卡/新天地大厅牛... 牛牛是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:8488009许多玩家在游戏中会购买房卡来享受更...
微信打炸金花链接房卡怎么买/怎... 炸金花是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:33903369许多玩家在游戏中会购买房卡来享...
微信玩炸金花房卡怎么买/开牛牛... 炸金花是一款非常受欢迎的棋牌游戏,咨询房/卡添加微信:55051770许多玩家在游戏中会购买房卡来享...