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

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 项目结构和运行效果
  • 3 .分片上传后端实现
    • 3.1 项目结构和技术介绍
    • 3.2 核心代码
  • 4. 项目运行测试
  • 5. 技术选型考量
  • 6. 项目源码
  • 参考链接

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

3 .分片上传后端实现

3.1 项目结构和技术介绍

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

image-20240609151213829

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) + "毫秒");
    }
}

4. 项目运行测试

测试效果如下:

录制_2024_06_09_15_32_22_335

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

image-20240609153437119

5. 技术选型考量

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

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

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

  • 实现复杂度增加:

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

  • 实际应用场景需求:

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

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

6. 项目源码

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)](

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

huggingface_hub LocalEntryNotFoundErroringface

报错详细 LocalEntryNotFoundError: An error happened while trying to locate the file on the Hub and we cannot find the requested files in the local cache. Please check your connection and try again or make sure your Internet connection is on.问题说明 在…

【UML用户指南】-15-对高级结构建模-对象图

目录 1、对象图的组成 2、 对象图和类图关系 3、对对象结构建模 4、逆向工程 5、对象图构建要求 对象图对包含在类图中的事物的实例建模。 对象图显示了在某一时间点上一组对象以及它们之间的关系。 对象图用于对系统的静态设计视图或静态交互视图建模。 对某一时刻的系统…

CentOS7下快速升级至OpenSSH9.7p2安全版本

一、CentOS7服务器上编译生成OpenSSH9.3p2的RPM包 1、编译打包的shell脚本来源于该项目 https://github.com/boypt/openssh-rpms解压zip项目包 unzip openssh-rpms-main.zip -d /opt cd /opt/openssh-rpms-main/ vim pullsrc.sh 修改第23行为source ./version.env 2、sh pull…

人工智能在肿瘤细胞分类中的应用|顶刊速递·24-06-06

小罗碎碎念 推文主题——人工智能在肿瘤细胞分类中的应用。 重点关注 临床方向的同学/老师建议重点关注第四篇&第六篇文章&#xff0c;最近DNA甲基化和蛋白组学与AI的结合&#xff0c;在顶刊中出现的频率很高&#xff0c;建议思考一下能否和自己的课题结合。 工科的同学重…

全网爆火【MBTI人格测试】是如何实现的?

功能介绍 概述 MBTI人格测试是一款基于Agent Builder框架开发的智能体应用&#xff0c;旨在通过五个精心设计的问题准确分析用户的MBTI性格类型。完成测试后&#xff0c;应用将提供详细的性格分析和建议&#xff0c;帮助用户更好地理解自己的性格特点。 功能详述 1. MBTI测试…

RAG实战4-RAG过程中发生了什么?

RAG实战4-RAG过程中发生了什么&#xff1f; 在RAG实战3中我们介绍了如何追踪哪些文档片段被用于检索增强生成&#xff0c;但我们仍不知道RAG过程中到底发生了什么&#xff0c;为什么大模型能够根据检索出的文档片段进行回复&#xff1f;本文将用一个简单的例子来解释前面的问题…

Linux磁盘管理(MBR、分区表、分区、格式化)

目录 1、简单介绍 2、MBR&#xff1a; 2.1、分区表&#xff1a; 2.2、注意&#xff1a; 2.3、编号问题&#xff1a; 2.4、磁盘的命名&#xff1a; 2.5、格式化分区 1、简单介绍 1.1、track&#xff1a;磁道&#xff0c;就是磁盘上同心圆&#xff0c;从外向里&#xff0c…

Imagic: Text-Based Real Image Editing with Diffusion Models

Imagic: Text-Based Real Image Editing with Diffusion Models Bahjat Kawar, Google Research, CVPR23, Paper, Code 1. 前言 在本文中&#xff0c;我们首次展示了将复杂&#xff08;例如&#xff0c;非刚性&#xff09;基于文本的语义编辑应用于单个真实图像的能力。例如…

[Redis] Redis Desktop Manager 安装包和连接和创建流程

1. 安装流程就是next&#xff0c;就可以。 2. 分别填写好&#xff1a; Name(自定义&#xff0c;redis这个库展示的名字), Host, Port, Auth(Redis 的连接password) 3. 要勾选上Use SSL Protocol 选项&#xff0c; 4. 连接到redis上&#xff0c;展示不同的database&#xff0c;…

为什么电容两端电压不能突变

我们先从RC延时电路说起吧&#xff0c;图1是最简单的RC延时电路&#xff0c;给一个阶跃的电压信号&#xff0c;电压会变成黄色曲线这个样子&#xff0c;这是为什么呢&#xff1f; 图1 电压跳变后&#xff0c;电源负极电子移动到电容下极板&#xff0c;排斥上极板电子流动到电源…

rtl8723du android5.1 6818 (wifi 部分)(第三部分)

这部分主要就是 应用了。具体的详细框架 在 android4.4 部分写的差不多的。 之前板卡依然是使用的 mt6620 ,所以在移植的过程中,需要把之前的 wifi 的驱动一点一点的去掉。 1 kernel 的修改。 将驱动拷贝到 wireless 下。 修改Kconfig 修改Makefile 2 驱动的Makefile 的修改…

C语言经典指针运算笔试题图文解析

指针运算常常出现在面试题中&#xff0c;画图解决是最好的办法。 题目1&#xff1a; #include <stdio.h> int main() {int a[5] { 1, 2, 3, 4, 5 };int* ptr (int*)(&a 1);printf("%d,%d", *(a 1), *(ptr - 1));return 0; } //程序的结果是什么&…

深度学习 --- stanford cs231 编程作业(assignment1,Q3: softmax classifier)

stanford cs231 编程作业(assignment1&#xff0c;Q3: softmax classifier softmax classifier和svm classifier的assignment绝大多部分都是重复的&#xff0c;这里只捡几个重点。 1&#xff0c;softmax_loss_naive函数&#xff0c;尤其是dW部分 1&#xff0c;1 正向传递 第i张…

银河麒麟v10 sp3编译制作内核rpm包——筑梦之路

环境信息 下载内核源码包 这里下载4.19版本的内核源码包&#xff0c;当前最新为4.19.315 https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.19.315.tar.xz 准备编译环境 # 安装编译需要的依赖包yum install rpm-devel rpmdevtools yum groupinstall "Developmen…

AI写作助手:简化你的学术写作流程

在日常工作和生活中&#xff0c;我经常使用各种各样的人工智能工具&#xff0c;如AI写作助手、AI语音助手和AI绘图工具等。这些AI工具显著提升了我的工作效率&#xff0c;并极大地简化了我的日常任务。作为一名AI工具的忠实爱好者&#xff0c;我搜集了许多免费的AI工具&#xf…

OZON多少资金可以提现,OZON提现要求

在跨境电商领域&#xff0c;OZON平台凭借其卓越的服务和广泛的用户基础&#xff0c;吸引了众多卖家入驻。然而&#xff0c;对于许多新手卖家而言&#xff0c;关于OZON平台的资金提现问题仍然存在一定的疑惑。本文将详细解析OZON平台上的资金提现政策&#xff0c;帮助卖家们更好…

JAVA小知识15:JAVA到底是值传递还是引用传递?

java中是值传递还是引用传递&#xff1f; 结论&#xff1a;值传递 一、值传递、引用传递 先看一个例子&#xff1a; 在这个例子中函数private static void updata(int i)中的 i 就叫形参 updata(a)中的 a 就叫做实参 值传递&#xff1a;若改变了i的值&#xff0c;但是a的值不变…

2025年最值得期待数据安全保护系统

虽然具体到2025年的数据安全保护系统细节可能尚未完全公开&#xff0c;但从当前趋势和技术创新来看&#xff0c;可以预见一些关键特性和系统可能的发展方向。 1.防泄密软件安企神 持续的技术创新&#xff1a;安企神将继续采用并升级其先进的加密技术&#xff0c;确保数据在传输…

【Mybatis】关于Mybatis手写xml文件的常见问题

明天就是端午节啦 博主今天先提前祝大家端午节快乐呀&#xff01;&#xff01; 文章目录 前言一、尽量做到参数化查询二、关于param注解三、mybatis处理特殊字符1.转义字符2.<![CDATA[]]>&#xff08;CDATA 标记&#xff09; 四、结果映射1、映射好文件与接口 namespace2…

Docker 容器 mysql 配置主从

1、前提条件 集群的条件下 服务器 172.16.11.195 13316:3306 服务器 172.16.11.196 13317:3306 配置好主数据库和从数据 2、配置主从数据库 2.1使用portainer 来管理容器 建立数据库密码 新增配置文件 # mysql-master.cnf [mysqld] server_id110 log-binmysql-binrela…