Springboot使用webupload大文件分片上传(包含前后端源码)

news2024/11/26 15:36:27

Springboot使用webupload大文件分片上传(包含源码)

  • 1. 实现效果
    • 1.1 分片上传效果图
    • 1.2 分片上传技术介绍
  • 2. 分片上传前端实现
    • 2.1 什么是WebUploader?
      • 功能特点
      • 接口说明
      • 事件API
      • Hook 机制
    • 2.2 前端代码实现
      • 2.2.1(不推荐)使用官方压缩文件方式引入
      • 2.2.2 (推荐)模块引入
      • 2.2.3 核心代码
      • 2.2.4 项目结构和运行效果
    • 2.3 分片上传后端实现
      • 2.3.1 项目结构和技术介绍
      • 2.3.2 核心代码
  • 3. 项目运行测试
  • 4. 技术选型考量
  • 5. 项目源码
  • 参考链接

1. 实现效果

1.1 分片上传效果图

如下上传过程的效果图,可以看到文件上传进度和浏览器控制台中打印的请求信息

效果图描述如下:

  1. 选择文件:这里我选择需要上传了1.09GB的pdf大文件
  2. 分片上传: 文件被切分为多个小片段(分片),每个分片独立上传,以提高上传效率和稳定性
  3. 进度条显示: 上传过程中显示文件上传进度条,实时反映上传进度
  4. 请求日志: 浏览器控制台打印每个分片上传的 HTTP 请求详情,包括请求头、请求体和服务器响应信息

录制_2024_06_08_19_19_36_376

1.2 分片上传技术介绍

本文使用技术栈:springboot、vue、webupload、mysql等

在项目开发中需要上传一个非常大的文件时,单次上传整个文件往往会遇到网络不稳定、带宽限制、上传失败等问题。为了解决这些问题,文件分片上传(也称为断点续传)应运而生。分片上传的核心思想是将一个大文件分成若干份大小相等的多个小块数据块(我们称之为 Part),等所有小块文件上传成功后,再将文件进行合并成完整的原始文件

文件分片上传的优点主要有以下几点:

  1. 断点续传:在网络中断或其他错误导致上传失败时,只需重新上传失败的部分,而不必从头开始上传整个文件,从而提高上传的可靠性和效率。
  2. 降低网络压力:分片上传可以控制每个片段的大小,避免一次性传输大量数据导致的网络拥堵,提高网络资源的利用率。
  3. 并行上传:多个分片可以同时上传,加快整体上传速度。
  4. 灵活处理:服务器可以更灵活地处理和存储文件分片,减少内存和带宽的占用。

本文使用 WebUploader 实现文件的分片上传。WebUploader 是一个由百度开发的强大而灵活的文件上传工具,支持文件分片上传、断点续传等功能。本文详细讲解并实现 WebUploader 的安装与配置,如何实现文件分片上传,以及如何在服务器端合并文件分片。通过这篇博客,你将学会:安装和配置 WebUploader实现文件分片上传

2. 分片上传前端实现

技术栈或技术点:vue、webuploader、elmentui

2.1 什么是WebUploader?

WebUploader 是由百度公司开发的一个现代文件上传组件,主要基于 HTML5,同时辅以 Flash 技术。它支持大文件的分片上传,提高了上传效率,并且兼容主流浏览器。

官网地址: [Web Uploader - Web Uploader (fex-team.github.io)](http://fex.baidu.com/webuploader/)

image-20240608212651303

功能特点

  1. 分片、并发上传: WebUploader 支持将大文件分割成小片段并行上传,极大地提高了上传效率。
  2. 预览、压缩: 支持常用图片格式(如 jpg、jpeg、gif、bmp、png)的预览和压缩,节省了网络传输数据量。
  3. 多途径添加文件: 支持文件多选、类型过滤、拖拽(文件和文件夹)以及图片粘贴功能。
  4. HTML5 & FLASH: 兼容所有主流浏览器,接口一致,不需要担心内部实现细节。
  5. MD5 秒传: 通过 MD5 值验证,避免重复上传相同文件。
  6. 易扩展、可拆分: 采用模块化设计,各功能独立成小组件,可自由组合搭配。

接口说明

WebUploader 提供了丰富的接口和钩子函数,以下是几个关键的接口:

  • before-send-file: 在文件发送之前执行。
  • before-file: 在文件分片后、上传之前执行。
  • after-send-file: 在所有文件分片上传完毕且无错误时执行。

WebUploader 的所有代码都在一个闭包中,对外只暴露了一个变量 WebUploader,避免与其他框架冲突。所有内部类和功能都通过 WebUploader 命名空间进行访问。

事件API

Uploader 实例拥有类似 Backbone 的事件 API,可以通过 onoffoncetrigger 进行事件绑定和触发。

uploader.on('fileQueued', function(file) {
    // 处理文件加入队列的事件
});

 this.uploader.on('uploadSuccess', (file, response) => {
     // 上传成功事件
});

除了通过 on 绑定事件外,还可以直接在 Uploader 实例上添加事件处理函数:

uploader.onFileQueued = function(file) {
    // 处理文件加入队列的事件
};

Hook 机制

关于hook机制的个人理解:Hook机制就像是在程序中的特定事件或时刻(比如做地锅鸡的时候)设定一些“钩子”。当这些事件发生时,程序会去“钩子”上找有没有要执行的额外功能,然后把这些功能执行一下。这就好比在做地锅鸡的过程中,你可以在某个步骤(比如炖鸡的时候)加上自己的调料或额外的配菜,来调整和丰富最终的味道,而不需要改动整体的食谱。

Uploader 内部功能被拆分成多个小组件,通过命令机制进行通信。例如,当用户选择文件后,filepicker 组件会发送一个添加文件的请求,负责队列的组件会根据配置项处理文件并决定是否加入队列。

webUploader.Uploader.register(
  {
    'before-send-file': 'beforeSendFile',
    'before-send': 'beforeSend',
    'after-send-file': 'afterSendFile'
  },
  {
    // 时间点1:所有分块进行上传之前调用此函数
    beforeSendFile: function(file) {
      // 利用 md5File() 方法计算文件的唯一标记符
      // 创建一个 deferred 对象
      var deferred = webUploader.Deferred();
      // 计算文件的唯一标记,用于断点续传和秒传
      // 请求后台检查文件是否已存在,实现秒传功能
      return deferred.promise();
    },
    // 时间点2:如果有分块上传,则每个分块上传之前调用此函数
    beforeSend: function(block) {
      // 向后台发送当前文件的唯一标记
      // 请求后台检查当前分块是否已存在,实现断点续传功能
      var deferred = webUploader.Deferred();
      return deferred.promise();
    },
    // 时间点3:所有分块上传成功之后调用此函数
    afterSendFile: function(file) {
      // 前台通知后台合并文件
      // 请求后台合并所有分块文件
    }
  }
);

2.2 前端代码实现

2.2.1(不推荐)使用官方压缩文件方式引入

首先我们需要下载官方文件,下载地址:Releases · fex-team/webuploader (github.com)

实现方式:快速开始 - Web Uploader (fex-team.github.io)

image-20240608213613872

下载文件webuploader-0.1.5.zip并解压后的文件内容如下:

image-20240608214247152

2.2.2 (推荐)模块引入

在已有项目或者新的空vue项目中先执行下列命令

# 引入分片需要
npm install webuploader
npm install jquery@1.12.4

image-20240608223139745

image-20240608223551207

2.2.3 核心代码

WebUpload.vue

<template>
  <div>
    <div class="container">
      <div class="handle-box">
        <el-button type="primary" id="picker" style="padding: 0px 14px" icon="el-icon-upload2">
          选择文件
        </el-button>
      </div>
      <el-table :data="internalFileListData" style="width: 100%">
        <el-table-column prop="fileName" label="文件名称"  align="center"></el-table-column>
        <el-table-column prop="fileSize" align="center" label="文件大小" width="150"></el-table-column>
        <el-table-column label="进度" align="center" width="300">
          <template slot-scope="scope">
            <div class="progress-container">
              <el-progress :text-inside="true" :stroke-width="15" :percentage="scope.row.percentage"></el-progress>
            </div>
          </template>
        </el-table-column>
        <el-table-column prop="speed" label="上传速度" align="center" width="150">
          <template slot-scope="scope">
            <div>{{ scope.row.speed }}</div>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="150" align="center">
          <template slot-scope="scope">
            <el-button type="text" icon="el-icon-close" class="red" @click="removeRow(scope.$index, scope.row)">
              移除
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>


<script>
import webUploader from 'webuploader' // 引入WebUploader库

export default {
  name: 'WebFileUpload',
  props: {
    headers: {
      type: String,
      default: ''
    },
    fileNumLimit: {
      type: Number,
      default: 100
    },
    fileSize: {
      type: Number,
      default: 1 * 1024 * 1024 * 1024 * 1024 // 1gb
    },
    chunkSize: {
      type: Number,
      default: 5 * 1024 * 1024 // 5mb
    },
    uploadSuffixUrl: {
      type: String,
      default: 'http://localhost:5590'
    },
    multiple: {
      type: Boolean,
      default: false // 是否支持多文件上传
    },
    options: {
      type: Object,
      default: () => ({
        fileType: 'doc,docx,pdf,xls,xlsx,jpg,jpeg,png,mp4,avi', // 允许上传的文件类型
        fileUploadUrl: '/v1/upload/zone/zoneUploadSE', // 分片上传接口
        headers: {}
      })
    },
    fileListData: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      uploader: null,
      percentage: 0, // 上传进度
      internalFileListData: [], // 使用内部数据属性来保存文件列表数据
      uploadStatus: '', // 上传状态
      fList: [],
      fileTimestamps: {} // 用于存储每个文件的时间戳
    }
  },
  watch: {
    fileListData(newValue) {
      // 当parentData变化时,执行相应逻辑
      this.internalFileListData = newValue
      console.log(this.internalFileListData)
    }
  },
  mounted() {
    this.internalFileListData = [...this.fileListData]
    this.initUploader()
    this.initEvents()
  },
  methods: {
    /**
     * 初始化上传组件
     */
    initUploader() {
      this.uploader = webUploader.create({
        auto: true, // 选完文件后,是否自动上传。
        resize: false, // 不压缩image
        swf: '../../../assets/Uploader.swf', // swf文件路径
        server: this.uploadSuffixUrl + this.options.fileUploadUrl, // 默认文件接收服务端。
        pick: {
          id: '#picker', // 上传按钮
          multiple: this.multiple // 是否开启文件多选,
        },
        accept: [
          {
            title: 'file',
            extensions: this.options.fileType,
            mimeTypes: this.buildFileType(this.options.fileType)
          }
        ],

        // 单位字节,如果图片大小小于此值,不会采用压缩。512k  512*1024,如果设置为0,原图尺寸大于设置的尺寸就会压缩;如果大于0,只有在原图尺寸大于设置的尺寸,并且图片大小大于此值,才会压缩
        compressSize: 0,
        fileNumLimit: this.fileNumLimit, //验证文件总数量, 超出则不允许加入队列,默认值:undefined,如果不配置,则不限制数量
        fileSizeLimit: 2 * 1024 * 1024 * 1024 * 1024, // 1kb=1024*1024,验证文件总大小是否超出限制, 超出则不允许加入队列。
        fileSingleSizeLimit: this.fileSize, //单个文件大小是否超出限制, 超出则不允许加入队列。
        chunkSize: this.chunkSize, // 单个分片大小为5MB,1024 * 1024 * 5表示5MB

        chunked: true, //是否开启分片上传
        threads: 8, // 并发上传数
        chunkRetry: 8, // 网络错误重试次数

        prepareNextFile: false, //在上传当前文件时是否准备好下一个文件

        // 上传时添加的请求头,例如需要传送token等
        // headers: {
        //   Authorization: 'Bearer ' + getToken()
        // }
      })
    },

    initEvents() {
      // 文件添加到队列
      this.uploader.on('fileQueued', file => {
        if (!this.multiple) {
          // 清空现有文件列表,实现只上传单个文件
          this.internalFileListData = []
        }

        // 生成唯一的时间戳并存储在 fileTimestamps 对象中
        const timestamp = Date.now().toString()
        this.fileTimestamps[file.id] = timestamp

        const fileSize = this.formatFileSize(file.size)
        this.internalFileListData.push({
          fileId: file.id,
          fileName: file.name,
          fileSize: fileSize,
          percentage: 0, // 初始化进度为0
          speed: '0KB/s', // 初始化速度
          state: '就绪'
        })
        this.uploadToServer() // 选择文件后直接开始上传
      })

      /**
       * 监听上传成功事件
       * @param file: 文件对象
       * @param : 服务器返回的数据
       */
      this.uploader.on('uploadSuccess', (file, response) => {
        this.fList = []
        // 如果code等于30000,表示上传成功
        if (response.code === 30000) {
          response.data.fileName = response.data.originalName
          response.data.percentage = this.internalFileListData[0].percentage
          response.data.fileSize = this.internalFileListData[0].fileSize
          response.data.speed = this.internalFileListData[0].speed
          this.fList.push(response.data)
          this.$emit('getFileList', this.fList)
          this.$message.success('上传完成')
        } else {
          this.$message.error('上传失败')
        }
      })

      /**
       * 监听上传错误事件
       * @param file: 文件对象
       * @param : 服务器返回的数据
       */
      this.uploader.on('uploadError', () => {
        this.$message.error('上传出错')
      })

      // 监听上传进度
      this.uploader.on('uploadProgress', (file, percentage) => {
        // 找到对应文件并更新进度
        let targetFile = this.internalFileListData.find(item => item.fileId === file.id)
        if (targetFile) {
          const currentTime = new Date().getTime()
          const elapsedTime = (currentTime - (targetFile.startTime || currentTime)) / 1000 // 秒
          const uploadedSize = percentage * file.size
          const speed = this.formatFileSize(uploadedSize / elapsedTime) + '/s'

          targetFile.percentage = parseFloat((percentage * 100).toFixed(2))
          targetFile.speed = speed
          targetFile.startTime = targetFile.startTime || currentTime
        }
      })

      // 上传之前发送的数据
      this.uploader.on('uploadBeforeSend', (block, data, headers) => {
        const fileTimestamp = this.fileTimestamps[block.file.id]
        data.fileMd5 = block.file.fileMd5
        data.contentType = block.file.type
        data.chunks = block.file.chunks
        data.zoneTotalMd5 = block.file.fileMd5
        data.zoneMd5 = block.zoneMd5
        data.zoneTotalCount = block.chunks
        data.zoneNowIndex = block.chunk
        data.zoneTotalSize = block.total
        data.zoneStartSize = block.start
        data.zoneEndSize = block.end
        data.fileUUID = fileTimestamp
        headers.Authorization = this.options.headers.Authorization
      })

      // 所有文件上传完成
      this.uploader.on('uploadFinished', () => {
        this.uploadBtnDisabled = false
        this.uploadStatus = 'el-icon-upload'
        // this.$message.success('文件上传完毕')
      })

      // 错误信息监听
      this.uploader.on('error', handler => {
        let errorMessage = ''
        if (handler === 'F_EXCEED_SIZE') {
          errorMessage =
            '上传的单个文件太大! 最大支持' +
            this.formatFileSize(this.fileSize) +
            '! 操作无法进行, 如有需求请联系管理员'
        } else if (handler === 'Q_TYPE_DENIED') {
          errorMessage = '不允许上传此类文件! 操作无法进行, 如有需求请联系管理员'
        }
        if (errorMessage) {
          this.$message.error({
            showClose: true,
            message: errorMessage
          })
        }
      })
    },
    uploadToServer() {
      if (this.internalFileListData.length <= 0) {
        this.$message.error({
          showClose: true,
          message: '没有上传的文件'
        })
        return
      }
      this.uploadBtnDisabled = true
      this.uploadStatus = 'el-icon-loading'
      this.uploader.upload()
    },

    /**
     * 格式化文件大小
     * @param {Number} size 文件大小
     * @return {String} 格式化后的文件大小
     */
    formatFileSize(size) {
      const units = ['KB', 'MB', 'GB']
      let unitIndex = -1
      do {
        size /= 1024
        unitIndex++
      } while (size >= 1024 && unitIndex < units.length - 1)
      return size.toFixed(2) + units[unitIndex]
    },

    /**
     * 构建文件类型字符串,以便在文件选择对话框中使用
     * @param {string} fileType - 用逗号分隔的文件扩展名字符串,例如 "jpg,png,gif"
     * @return {string} - 以逗号分隔的文件类型字符串,每个扩展名前加一个点,例如 ".jpg,.png,.gif"
     */
    buildFileType(fileType) {
      const fileTypes = fileType.split(',')
      return fileTypes.map(type => `.${type}`).join(',')
    },

    /**
     * 操作中的移除
     * @param {Number} index - 文件列表索引
     * @param {Object} row - 文件对象
     */
    removeRow(index, row) {
      this.internalFileListData.splice(index, 1)

      const files = this.uploader.getFiles()
      for (let i = 0; i < files.length; i++) {
        if (files[i].id === row.fileId) {
          this.uploader.removeFile(files[i], true)
          break
        }
      }
      this.$emit('removeRow', index)
    }
  }
}
</script>

<style>
.container {
  margin-left: 50px;
  width: 100%;
  padding: 30px;
  background: #fff;
  border: 1px solid #ddd;
  border-radius: 5px;
}

.handle-box {
  margin-bottom: 20px;
}

#picker div:nth-child(2) {
  width: 100% !important;
  height: 100% !important;
}

.webuploader-container {
  position: relative;
}

.webuploader-element-invisible {
  position: absolute !important;
  clip: rect(1px, 1px, 1px, 1px);
}

.webuploader-pick {
  line-height: 39px;
  margin-right: 20px;
}

.webuploader-pick-hover {
  background: #409eff;
}

.progress-container {
  width: 200px; /* 设置进度条容器的宽度 */
  margin: 0 auto;
}
</style>

App.vue

<template>
  <div id="app">
    <main>
      <el-form :span="20">
        <el-col :span="20">
          <el-form-item>
            <!-- 分片上传组件 -->
            <WebUpload></WebUpload>
          </el-form-item>
        </el-col>
      </el-form>
    </main>
  </div>
</template>

<script>
import WebUpload from './components/WebUpload.vue'

export default {
  name: 'App',
  components: {
    WebUpload
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

同时使用了样式,因此需要引入element-ui

npm install element-ui -S

# main.js中内容
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';

Vue.use(ElementUI);

new Vue({
  el: '#app',
  render: h => h(App)
});

2.2.4 项目结构和运行效果

执行npm run sever运行后页面效果和最终项目代码结构

image-20240609150500553

2.3 分片上传后端实现

2.3.1 项目结构和技术介绍

后端使用技术栈主要是springboot,引入了mybatis-plus,数据库使用mysql

image-20240609151213829

2.3.2 核心代码

控制类:FileUploadController.java

package com.example.zhou.controller;

import com.example.zhou.common.Result;
import com.example.zhou.service.IFileZoneRecordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;

/**
 * @author ZhouQuan
 * @desciption 文件上传操作录控制类
 * @date 2024/5/4 17:09
 */
@Slf4j
@RestController
@RequestMapping("/v1/upload/zone")
public class FileUploadController {

    @Resource
    private IFileZoneRecordService iFileZoneRecordService;

    /**
     * 单个大文件分片上传-不使用md5
     *
     * @param file           分片的文件
     * @param zoneTotalCount 分片总数
     * @param zoneTotalSize  文件总大小
     * @param zoneNowIndex   当前分片编号
     * @param fileUUID       每个文件上传时文件唯一标识
     * @return code: 30000 文件上传成功
     * @return code: 30002 分片上传成功
     */
    @PostMapping("/zoneUploadSE")
    public Result zoneUploadSE(MultipartFile file,
                               Integer zoneNowIndex,
                               Integer zoneTotalCount,
                               Integer zoneTotalSize,
                               String fileUUID) {
        return iFileZoneRecordService.zoneUploadSE(file, zoneNowIndex, zoneTotalCount, zoneTotalSize, fileUUID);
    }
}

核心实现方法:FileZoneRecordServiceImpl.java

package com.example.zhou.service.impl;

import com.example.zhou.common.Result;
import com.example.zhou.common.ResultCode;
import com.example.zhou.config.FileUploadConfig;
import com.example.zhou.entity.Archive;
import com.example.zhou.mapper.ArchiveMapper;
import com.example.zhou.service.IFileZoneRecordService;
import com.example.zhou.utils.IdUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Date;

@Slf4j
@Service
public class FileZoneRecordServiceImpl  implements IFileZoneRecordService {

    @Resource
    private ArchiveMapper archiveMapper;

    @Resource
    private FileUploadConfig fileUploadConfig;

    public Result zoneUploadSE(MultipartFile multipartFile,
                               Integer currentChunk,
                               Integer zoneTotalCount,
                               Integer zoneTotalSize,
                               String fileUUID) {
        try {
            // 获取上传文件的原始文件名和扩展名
            String originalName = multipartFile.getOriginalFilename();
            String extension = FilenameUtils.getExtension(originalName);

            // 构建上传路径
            String uploadPath = Paths.get(fileUploadConfig.getUploadFolder(), extension).toString();
            FileUtils.forceMkdir(new File(uploadPath)); // 创建目录(如果不存在)

            // 写入临时文件
            String tempFileName = (currentChunk != null) ? currentChunk + "_" + fileUUID + "_" + originalName :
                    fileUUID + "_" + originalName;
            File tempFile = new File(uploadPath, tempFileName);
            multipartFile.transferTo(tempFile);

            // 如果是最后一个分片或者只有一个分片,进行合并操作
            if (currentChunk == null || (currentChunk == zoneTotalCount - 1)) {
                // 获取最终文件路径
                String finalFileName = fileUUID + "_" + originalName;
                File finalFile = new File(uploadPath, finalFileName);

                // 合并分片文件
                mergeChunkFiles(uploadPath, fileUUID, originalName, zoneTotalCount, finalFile);

                // 移动文件到指定目录 示例:pdf/2024/24/uuid.pdf
                Path filePath = Paths.get(extension,  DateFormatUtils.format(new Date(), "yyyy/MM/dd"),
                        IdUtils.fastUUID() + "." + extension);

                // 移动文件位置到指定文件夹下
                FileUtils.moveFile(finalFile,
                        new File(Paths.get(fileUploadConfig.getUploadFolder(), filePath.toString()).toString()));

                // 保存附件信息到数据库
                Archive archive = new Archive();
                archive.setSid(IdUtils.fastUUID());
                archive.setFileName(filePath.getFileName().toString());
                archive.setOriginalName(originalName);
                archive.setPath(filePath.toString());
                archive.setSize(zoneTotalSize != null ? zoneTotalSize : (int) tempFile.length());
                archive.setFileType(extension);

                // 插入数据库
                int result = archiveMapper.insert(archive);
                return new Result(ResultCode.FILEUPLOADED, archive);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
        return new Result(ResultCode.ZONEUPLOADED, "分片上传成功");
    }

    private void mergeChunkFiles(String uploadPath, String fileUUID, String fileName, Integer zoneTotalCount,
                                 File finalFile) throws IOException {
        long start = System.currentTimeMillis();
        try (BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(finalFile))) {
            for (int i = 0; i < zoneTotalCount; i++) {
                File chunkFile = new File(uploadPath, i + "_" + fileUUID + "_" + fileName);
                while (!chunkFile.exists()) {
                    try {
                        Thread.sleep(100); // 休眠100毫秒后重新判断
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                log.info("正在合并分片文件:" + chunkFile.getName());
                // 读入分片数据并写入最终文件
                try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(chunkFile))) {
                    byte[] buffer = new byte[8192]; // 8KB缓冲区
                    int bytesRead;
                    while ((bytesRead = bis.read(buffer)) != -1) {
                        os.write(buffer, 0, bytesRead);
                    }
                }
                // 删除已合并的分片文件
                long deleteStart = System.currentTimeMillis();
                if (!chunkFile.delete()) {
                    log.warn("删除分片文件失败:" + chunkFile.getName());
                } else {
                    log.info("删除分片耗时:" + (System.currentTimeMillis() - deleteStart) + "毫秒");
                }
            }
        }
        long end = System.currentTimeMillis();
        log.info("文件合并完成,耗时:" + (end - start) + "毫秒");
    }
}

3. 项目运行测试

测试效果如下:

录制_2024_06_09_15_32_22_335

后端返回结果中会返回文件信息给前端,可根据业务存储文件sid或者是路径信息

image-20240609153437119

4. 技术选型考量

本文主要是使用了分片上传,其实并未使用计算文件md5来实现断点续传和文件秒传,主要考量如下:

  • MD5 性能开销大且校验耗时:

    计算大文件的 MD5 哈希值是一个耗时的操作,特别是对于数 GB 的大文件。这个过程会占用大量的 CPU 资源,并增加上传前的等待时间,从而降低用户体验。

  • 实现复杂度增加:

    引入 MD5 校验需要在客户端和服务器端进行额外的处理逻辑,包括计算文件的 MD5 值、校验分片的完整性等。这会增加开发和维护的复杂度。

  • 实际应用场景需求:

    • 在某些应用场景中,断点续传和秒传功能并不是必需的。比如用户可以在一次会话中完成大文件上传,或者文件上传失败的概率较低时,不使用 MD5 校验也能满足需求。

基于以上考虑选择了更为简洁和高效的实现方案,不使用 MD5 校验。这种方案可以显著减少上传前的准备时间和计算开销,简化系统的实现和维护,同时在大多数情况下也能满足实际需求。

5. 项目源码

image-20240609155255125

https://zhouquanquan.lanzn.com/b00g2crzsh
密码:h5iu

参考链接

  1. 官方地址 https://github.com/fex-team/webuploader

  2. 基于SpringBoot和WebUploader实现大文件分块上传.断点续传.秒传-阿里云开发者社区 (aliyun.com)

  3. 在Vue项目中使用WebUploader实现文件上传_vue webuploader-CSDN博客

  4. vue中大文件上传webuploader前端用法_vue webuploader 大文件上传-CSDN博客

  5. SpringBoot实现大文件上传/下载(分片、断点续传) - helloliyh - 博客园 (cnblogs.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1805560.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ssm汽车在线销售系统

摘 要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识&#xff0c;科学化的管理&#xff0c;使信息存…

python中使用 Matplotlib 的 GridSpec 来实现更复杂的布局控制

matplotlib.gridspec 是 Matplotlib 库中的一个模块&#xff0c;用于创建复杂的子图布局。GridSpec 提供了更精细的控制&#xff0c;允许你定义不同大小和位置的子图。下面是对 GridSpec 的详细介绍和一些常见用法示例&#xff1a; 1. 基本用法 GridSpec 类似于表格布局&…

R语言数据分析16-针对芬兰污染指数的分析与考察

1. 研究背景及意义 近年来&#xff0c;随着我国科技和经济高速发展&#xff0c;人们生活质量也随之显著提高。但是&#xff0c; 环境污染问题也日趋严重&#xff0c;给人们的生活质量和社会生产的各个方面都造成了许多不 利的影响。空气污染作为环境污染主要方面&#xff0c;更…

FCN-语义分割中的全卷积网络

FCN-语义分割中的全卷积网络 语义分割 语义分割是计算机视觉中的关键任务之一&#xff0c;现实中&#xff0c;越来越多的应用场景需要从影像中推理出相关的知识或语义&#xff08;即由具体到抽象的过程&#xff09;。作为计算机视觉的核心问题&#xff0c;语义分割对于场景理…

QT C++(QT控件 QPushButton,QRadioButton,QCheckBox)

文章目录 1. QPushButton 普通按钮2. QRadioButton 单选按钮3. QCheckBox 复选按钮 1. QPushButton 普通按钮 QPushButton中的重要属性 text&#xff1a;按钮中的文本icon&#xff1a;按钮的图标iconSize&#xff1a;按钮中图标的尺寸shortCut&#xff1a;按钮对应的快捷键&a…

关于烫烫烫和屯屯屯

微较的msvc编译器&#xff0c;调试模式下为了方便检测内存的非法访问&#xff0c;对于不同的内存做了初始化&#xff0c; 未初始化栈&#xff1a; 0xCCCCCCCC 未初始化堆&#xff1a; 0xCDCDCDCD 已释放的堆&#xff1a; 0xDDDDDDDD 0xCCCC解释为GB2312字符即是烫&#xff…

“深入探讨Java中的对象拷贝:浅拷贝与深拷贝的差异与应用“

前言&#xff1a;在Java编程中&#xff0c;深拷贝&#xff08;Deep Copy&#xff09;与浅拷贝&#xff08;Shallow Copy&#xff09;是两个非常重要的概念。它们涉及到对象在内存中的复制方式&#xff0c;对于理解对象的引用、内存管理以及数据安全都至关重要。 ✨✨✨这里是秋…

AI视频教程下载:如何用ChatGPT来求职找工作?

这是一个关于使用ChatGPT找工作的课程&#xff0c;作者分享了自己的求职经验和技巧&#xff0c;介绍了如何使用人工智能来改进个人资料和简历&#xff0c;以及如何研究公司和面试。通过细节处理职业目标、分享个人兴趣和技能、寻求导师和专业发展机会&#xff0c;以及在行业内建…

【K8s源码分析(三)】-K8s调度器调度周期介绍

本文首发在个人博客上&#xff0c;欢迎来踩&#xff01; 本次分析参考的K8s版本是v1.27.0。 K8s的整体调度框架如下图所示。 调度框架顶层函数 K8s调度器调度的核心函数schedulerone在pkg/scheduler/schedule_one.go:62&#xff0c;如下&#xff0c;这里将一些解释写在了注…

CTF Show MISC做题笔记

MISCX 30 题目压缩包为misc2.rar,其中包含三个文件:misc1.zip, flag.txt, hint.txt。其中后两个文件是加密的。 先解压出misc1.zip, 发现其中包含两个文件&#xff1a;misc.png和music.doc。其中后面文件是加密的。 解压出misc.png,发现图片尾部有消息&#xff1a;flag{flag…

Autosar Dem配置-Condition(TRC)的使用-基于ETAS软件

文章目录 前言Dem配置DemEnableConditionDemEnableConditionIdDemEnableConditionStatus DemEnableConditionGroupDemEventParameter 接口配置代码实现总结 前言 在车辆工作状态下&#xff0c;每个DTC检测可能都需要一个前提条件&#xff0c;否则如果任何条件下都可以进行DTC检…

【ARM Cache 与 MMU 系列文章 7.3 – ARMv8/v9 MMU 块描述符与页表描述符】

请阅读【ARM Cache 及 MMU/MPU 系列文章专栏导读】 及【嵌入式开发学习必备专栏】 上篇文章&#xff1a;【ARM Cache 系列文章 7.2 – ARMv8/v9 MMU 页表配置详细介绍 03 】 文章目录 MMU 块描述符与页描述符Block DescriptorBlock descriptor formatsBlock Entry 介绍Block En…

【C#】开发过程中记录问题

1.DateTimePicker控件获取时间 拖动控件,设置属性format为custom格式。例如我想获得20240101这种类型的string类型的数据: string DateTime = DateTimePicker.Value.ToString("yyyyMMdd");2.ComboBox下拉列表控件 默认为DropDown,下拉可修改。 DropDownList为下…

《Windows API每日一练》

2.2.8 第15练&#xff1a;处理WM_CLOSE消息 /*------------------------------------------------------------------------ 015 编程达人win32 API每日一练 第15个例子WM_CLOSE.C&#xff1a;回调函数---处理WM_CLOSE消息 WM_CLOSE消息 DestroyWindow函数 注意&#xf…

SprirngBoot+Vue房屋租赁系统(前后端分离)

技术栈 JavaSpringBootMavenMySQLMyBatisVueShiroElement-UI 角色对应功能 租客管理员 功能截图

Git【版本控制命令】

02 【本地库操作】 1.git的结构 2.Git 远程库——代码托管中心 2.1 git工作流程 代码托管中心用于维护 Git 的远程库。包括在局域网环境下搭建的 GitLab 服务器&#xff0c;以及在外网环境下的 GitHub 和 Gitee (码云)。 一般工作流程如下&#xff1a; 1&#xff0e;从远程…

[Cesium学习]

Popup弹窗 Cesium点位弹窗_cesium popup弹窗-CSDN博客 Cesium构造popup弹窗函数_cesium popup-CSDN博客 开发之家 - Cesium构造popup弹窗函数 GitHub - cesium-plugin/cesium-popup-es6: 气泡弹窗 热力图分析 // 创建Cesium Viewer实例 const viewer new Cesium.Viewer(c…

C#中使用Mysql批量新增数据 MySqlBulkCopy

在C#中使用MySqlBulkCopy类来批量复制数据到MySQL数据库&#xff0c;首先需要确保你的项目中已经引用了MySQL Connector。以下是使用MySqlBulkCopy的基本步骤&#xff1a; 1.安装MySQL Connector。 可以通过NuGet安装MySQL Connector&#xff1a; 2.在代码中引用必要的命名空间…

安装 JDK 17

安装包 百度网盘 提取码&#xff1a;6666 安装步骤 双击下载得到的安装包&#xff0c;开始安装&#xff1a; 正在安装&#xff1a; 安装完成&#xff1a; 安装路径下&#xff0c;多出来了很多新的内容。安装文件夹所包含的内容及作用&#xff1a; src 是 JDK 的源码包。类库…

DevExpress WPF中文教程:Grid - 如何向项目添加GridControl并绑定到数据

DevExpress WPF拥有120个控件和库&#xff0c;将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpress WPF能创建有着强大互动功能的XAML基础应用程序&#xff0c;这些应用程序专注于当代客户的需求和构建未来新一代支持触摸的解决方案。 无论是Office办公软件…