Java实现文件分片上传

2023-11-10

为什么需要文件分片上传

  1. 大文件上传中断:假如我们有一个5G的文件,上传过程中突然中断我们该怎么办?
  2. 上文件上传响应时间长:假如我们有个10G的文件,单次上传时间长,用户体验长,该怎么办?
  3. 大文件上传重复上传:某些大文件,我们已经上传过了,我们不想再一次上传,该怎么办?

(实践)分片上传

设计思路概述

如下图,我们会将一个大文件进行切片,然后调用文件上传接口,将分片base64数据、源文件名称、分片大小、分片个数、索引号传到服务器上。

在这里插入图片描述

分片上传表设计

为了保存分片上传进度,笔者创建了下面这样一张表,这张表记录了上传的文件以及文件分片上传的进度。
当我们上传的分片是第一个分片时,我们就会插入一条数据,记录文件名、分片索引号等信息。
后续上传成功的分片则都是更新信息shard_index这个字段。

DROP TABLE IF EXISTS `file`;
CREATE TABLE `file` (
  `id` char(8) NOT NULL DEFAULT '' COMMENT 'id',
  `path` varchar(100) NOT NULL COMMENT '相对路径',
  `name` varchar(100) DEFAULT NULL COMMENT '文件名',
  `suffix` varchar(10) DEFAULT NULL COMMENT '后缀',
  `size` int(11) DEFAULT NULL COMMENT '大小|字节B',
  `use` char(1) DEFAULT NULL COMMENT '用途|枚举[FileUseEnum]COURSE("C", "讲师"), TEACHER("T", "课程")',
  `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
  `updated_at` datetime(3) DEFAULT NULL COMMENT '修改时间',
  `shard_index` int(11) DEFAULT NULL COMMENT '已上传分片',
  `shard_size` int(11) DEFAULT NULL COMMENT '分片大小|B',
  `shard_total` int(11) DEFAULT NULL COMMENT '分片总数',
  `key` varchar(32) DEFAULT NULL COMMENT '文件标识',
  PRIMARY KEY (`id`),
  UNIQUE KEY `path_unique` (`path`),
  UNIQUE KEY `key_unique` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件';

接口设计

注意,以下参数都是必传参数:

  1. name:文件名称(包含文件后缀)
  2. shard:文件base64值。
  3. size:文件总大小。
  4. shardTotal:文件总分片数。
  5. shardSize:每个分片最大值,注意这里笔者说的分片最大值,原因很简单,如果我们文件分片大小。为10M,很可能最后一个分片大小不足10M,所以这个参数传的就是切割分片后的最大值。
  6. suffix:文件后缀,视频后缀为mp4,图片则为jpg等。

分片上传接口实现思路

  1. 必传参数校验。
  2. 获取文件base64值,并转为MultipartFile
  3. 获取本地存储路径,并判断该路径是否存在,如果不存在,则创建路径。
  4. 根据分片索引创建本地分片全路径,将MultipartFile写到这个路径中。
  5. 写入完成,记录文件上传进度,若这个文件之前都没有上传过则插入一条新数据,反之更新索引字段那一栏的数据。
  6. 判断本次上传的图片的索引号是否和文件总大小一致,如果一致说明上传完成开始进行文件合并,合并完成后,删除临时分片。

后端代码

在进行分片上传逻辑编写前,我们必须配置一下本地存储路径和映射地址,如下所示,笔者将file.path设置为本地文件存储路径。将file.domain设置为file.path对外的映射地址。

file.path=F:/video/
file.domain=http://127.0.0.1:9000/file/f/

为了做到这一点,笔者创建了一个SpringMvcConfig 配置,这样以来,我们在浏览器中键入http://127.0.0.1:9000/file/f/1.jpg就相当于访问文件服务器的F:/video/1.jpg

@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {


    @Value("${file.path}")
    private String FILE_PATH;


   

    /**
     * 文件查看的映射
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/f/**").addResourceLocations("file:"+FILE_PATH);
    }
}

完成这个步骤之后我们就可以开始编写后端代码了,为了方便读者理解,笔者基于上述编码思路一段一段讲述代码实现思路

首先是参数校验,这一步无非是判空,所以笔者就补贴出完整代码,这里就把方法贴出来让读者了解一下即可 。

  //1. 参数非空检查
        checkParams(fileDto);

参数校验无误后,将base64转为MultipartFile

//将base64转MultipartFile
MultipartFile multipartFile = Base64ToMultipartFile.base64ToMultipart(fileDto.getShard());

这个工具类实现思路也很简单Base64ToMultipartFile,根据base64封号切割成数组,数组[0]base64的描述信息,数组[1]则是具体内容,进行转码组装,具体参见下属代码:

public class Base64ToMultipartFile implements MultipartFile {

    private final byte[] imgContent;
    private final String header;

    public Base64ToMultipartFile(byte[] imgContent, String header) {
        this.imgContent = imgContent;
        this.header = header.split(";")[0];
    }

   ......@Override
    public void transferTo(File dest) throws IOException, IllegalStateException {
        new FileOutputStream(dest).write(imgContent);
    }

    public static MultipartFile base64ToMultipart(String base64) {
        try {
            String[] baseStrs = base64.split(",");

            BASE64Decoder decoder = new BASE64Decoder();
            byte[] b = new byte[0];
            b = decoder.decodeBuffer(baseStrs[1]);

            for(int i = 0; i < b.length; ++i) {
                if (b[i] < 0) {
                    b[i] += 256;
                }
            }

            return new Base64ToMultipartFile(b, baseStrs[0]);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

base64数据结构如下图所示:

在这里插入图片描述

获取本地存储路径,若不存在则创建

 //获取本地文件夹地址,拼接传入的参数use,得到一个本地文件夹路径,并判断是否存在,若不存在则直接创建
        String localDirPath = FILE_PATH + FileUseEnum.getByCode(fileDto.getUse());
        logger.info("本地文件夹地址:{}", localDirPath);
        File dirFile = new File(localDirPath);

        //如果目标文件夹不存在,则直接创建一个
        if (!dirFile.exists() && !dirFile.mkdirs()) {
            logger.error("文件夹创建失败,待创建路径:{}", localDirPath);
            throw new Exception("文件夹创建失败,创建路径:" + localDirPath);
        }

组装文件路径和分片路径,并将文件数据写入到分片路径中。

 //本地文件全路径
        String fileFullPath = localDirPath +
                File.separator +
                fileDto.getKey() +
                "." +
                fileDto.getSuffix();

        //创建文件分片全路径,将multipartFile写入到这个路径中
        String fileShardFullPath = fileFullPath +
                "." +
                fileDto.getShardIndex();

        multipartFile.transferTo(new File(fileShardFullPath));

成功完成文件上传后,更新文件信息表上传进入,如果表中没有这个文件的信息,则插入一条数据。反之更新文件信息表分片索引号字段。

  //更新文件表信息,无上传过这个文件则插入一条,反之直接更新索引值
        String relaPath = FileUseEnum.getByCode(fileDto.getUse()) + "/" + fileDto.getKey() + "." + fileDto.getSuffix();
        fileDto.setPath(relaPath);
        fileService.save(fileDto);

可以看到save的逻辑如下所示

 /**
     * 如果这个文件之前都没上传过则插入一条数据,反之更新索引只即可
     */
    public void save(FileDto fileDto) {
        File file = CopyUtil.copy(fileDto, File.class);
        File fileDb = selectByKey(fileDto.getKey());
        if (fileDb == null) {
            this.insert(file);
        } else {
            fileDb.setShardIndex(fileDto.getShardIndex());
            this.update(fileDb);
        }
    }

如果当前上传的分片为最后一个分片,则进行文件合并

 //判断当前分片索引是否等于分片总数,如果等于分片总数则执行文件合并
        if (fileDto.getShardIndex().equals(fileDto.getShardTotal())) {
            fileDto.setPath(fileFullPath);
            //文件合并
            merge(fileDto);
        }

组装结果并返回

ResponseDto responseDto = new ResponseDto();
        FileDto result = new FileDto();
        //设置文件映射地址给前端
        result.setPath(FILE_DOMAIN + "/" + relaPath);
        responseDto.setContent(result);


        logger.info("文件分片上传结束,请求结果:{}", JSON.toJSONString(responseDto));
        return responseDto;

文件合并逻辑,这里就比较简单了,创建输入流和输出流,以追加的形式将每个分片都写到输出文件中。

private void merge(FileDto fileDto) {
        logger.info("文件分片合并开始,请求参数:{}", JSON.toJSONString(fileDto));
        String path = fileDto.getPath();
        try (OutputStream outputStream = new FileOutputStream(path, true)) {
            for (Integer i = 1; i <= fileDto.getShardTotal(); i++) {
                try (FileInputStream inputStream = new FileInputStream(path + "." + i);) {
                    byte[] bytes = new byte[10 * 1024 * 1024];
                    int len;
                    while ((len = inputStream.read(bytes)) != -1) {
                        outputStream.write(bytes, 0, len);
                    }
                }

            }
        } catch (Exception e) {
            logger.error("文件合并失败,失败原因:{}", e.getMessage(), e);
        }

		System.gc();
        //删除所有分片
        for (Integer i = 1; i <= fileDto.getShardTotal(); i++) {
            File file = new File(path + "." + i);
            file.delete();
        }


    }

注意,这里有时需要考虑一个文件,就是文件合并后无论流如何关闭,在JVM进行gc回收之前,这些文件的资源在进行删除操作时可能会失败,所以稳妥起见,笔者尝试过调用System.gc()这个方法进行兜底。

这种方法不太符合编码规范,所以笔者建议使用定时任务等方式每日按时按点进行删除。

在这里插入图片描述

完整代码

@RequestMapping("/upload")
    public ResponseDto uploadShard(@RequestBody FileDto fileDto) throws Exception {
        logger.info("文件分片上传请求开始,请求参数: {}", JSON.toJSONString(fileDto));
        //1. 参数非空检查
        checkParams(fileDto);


        //将base64转MultipartFile
        MultipartFile multipartFile = Base64ToMultipartFile.base64ToMultipart(fileDto.getShard());


        //获取本地文件夹地址,拼接传入的参数use,得到一个本地文件夹路径,并判断是否存在,若不存在则直接创建
        String localDirPath = FILE_PATH + FileUseEnum.getByCode(fileDto.getUse());
        logger.info("本地文件夹地址:{}", localDirPath);
        File dirFile = new File(localDirPath);

        //如果目标文件夹不存在,则直接创建一个
        if (!dirFile.exists() && !dirFile.mkdirs()) {
            logger.error("文件夹创建失败,待创建路径:{}", localDirPath);
            throw new Exception("文件夹创建失败,创建路径:" + localDirPath);
        }

        //本地文件全路径
        String fileFullPath = localDirPath +
                File.separator +
                fileDto.getKey() +
                "." +
                fileDto.getSuffix();

        //创建文件分片全路径,将multipartFile写入到这个路径中
        String fileShardFullPath = fileFullPath +
                "." +
                fileDto.getShardIndex();

        multipartFile.transferTo(new File(fileShardFullPath));

        //更新文件表信息,无上传过这个文件则插入一条,反之直接更新索引值
        String relaPath = FileUseEnum.getByCode(fileDto.getUse()) + "/" + fileDto.getKey() + "." + fileDto.getSuffix();
        fileDto.setPath(relaPath);
        fileService.save(fileDto);

        //判断当前分片索引是否等于分片总数,如果等于分片总数则执行文件合并
        if (fileDto.getShardIndex().equals(fileDto.getShardTotal())) {
            fileDto.setPath(fileFullPath);
            //文件合并
            merge(fileDto);
        }

        ResponseDto responseDto = new ResponseDto();
        FileDto result = new FileDto();
        //设置文件映射地址给前端
        result.setPath(FILE_DOMAIN + "/" + relaPath);
        responseDto.setContent(result);


        logger.info("文件分片上传结束,请求结果:{}", JSON.toJSONString(responseDto));
        return responseDto;
    }



 /**
     * 文件合并
     *
     * @param fileDto
     */
    private void merge(FileDto fileDto) {
        logger.info("文件分片合并开始,请求参数:{}", JSON.toJSONString(fileDto));
        String path = fileDto.getPath();
        try (OutputStream outputStream = new FileOutputStream(path, true)) {
            for (Integer i = 1; i <= fileDto.getShardTotal(); i++) {
                try (FileInputStream inputStream = new FileInputStream(path + "." + i);) {
                    byte[] bytes = new byte[10 * 1024 * 1024];
                    int len;
                    while ((len = inputStream.read(bytes)) != -1) {
                        outputStream.write(bytes, 0, len);
                    }
                }

            }
        } catch (Exception e) {
            logger.error("文件合并失败,失败原因:{}", e.getMessage(), e);
        }


        //删除所有分片
        for (Integer i = 1; i <= fileDto.getShardTotal(); i++) {
            File file = new File(path + "." + i);
            file.delete();
        }


    }

(完善)断点续传和极速妙传

极速妙传设计思路

因为是大文件,我们很可能文件传一半就因为各种原因导致中断,我们的实现思路是和前端约定好为系统的文件都生成一个key,后端用这个key到数据库中查询是否存在上传记录。
如果有结果则结果返回给前端。

然后前端进行如下操作:

  1. 如果返回结果不为空,则记录中已上传索引值等于文件分片数,则说明之前上传过相同的文件,直接告知用户极速妙传成功。
  2. 如果返回结果不为空,已上传的索引值不等于文件分片数,则基于这个索引值+1完成断点续传。

后端代码实现

/**
     * 文件上传进度检查接口
     *
     * @param key
     * @return
     */
    @GetMapping("/check/{key}")
    public ResponseDto check(@PathVariable String key) {
        logger.info("文件上传进度检查接口请求开始,请求参数:{}", key);
        if (StringUtils.isEmpty(key)) {
            throw new BusinessException(BusinessExceptionCode.ILLEGAL_ARGUMENT_EXCEPTION);
        }

        FileDto fileDto = fileService.findByKey(key);
        ResponseDto responseDto = new ResponseDto();

        //如果不为空,则返回映射地址以及文件上传进度
        if (fileDto != null) {
            //将文件映射地址告知前端
            fileDto.setPath(FILE_DOMAIN + "/" + fileDto.getPath());
            responseDto.setContent(fileDto);
        }

        return responseDto;

    }

对接,前端代码实现

前端文件分片计算代码逻辑(ps:笔者基于Vue写的)

getFileShard (shardIndex, shardSize) {
                let _this = this;
                let file = _this.$refs.file.files[0];
                let start = (shardIndex - 1) * shardSize;	//当前分片起始位置
                let end = Math.min(file.size, start + shardSize); //当前分片结束位置
                let fileShard = file.slice(start, end); //从文件中截取当前的分片数据
                return fileShard;
            },

调用极速妙传和断点续传的逻辑

/**
             * 检查文件状态,是否已上传过?传到第几个分片?
             */
            check (param) {
                let _this = this;
                _this.$ajax.get(process.env.VUE_APP_SERVER + '/file/admin/check/' + param.key).then((response)=>{
                    let resp = response.data;
                    if (resp.success) {
                        let obj = resp.content;
                        //如果不存在则从第一个分片开始上传
                        if (!obj) {
                            param.shardIndex = 1;
                            console.log("没有找到文件记录,从分片1开始上传");
                            _this.upload(param);
                        } else if (obj.shardIndex === obj.shardTotal) {
                            // 已上传分片 = 分片总数,说明已全部上传完,不需要再上传
                            Toast.success("文件极速秒传成功!");
                            _this.afterUpload(resp);
                            $("#" + _this.inputId + "-input").val("");
                        }  else {
                            param.shardIndex = obj.shardIndex + 1;
                            console.log("找到文件记录,从分片" + param.shardIndex + "开始上传");
                            _this.upload(param);
                        }
                    } else {
                        Toast.warning("文件上传失败");
                        $("#" + _this.inputId + "-input").val("");
                    }
                })
            },

            /**
             * 将分片数据转成base64进行上传
             */
            upload (param) {
                let _this = this;
                let shardIndex = param.shardIndex;
                let shardTotal = param.shardTotal;
                let shardSize = param.shardSize;
                let fileShard = _this.getFileShard(shardIndex, shardSize);
                // 将图片转为base64进行传输
                let fileReader = new FileReader();

                Progress.show(parseInt((shardIndex - 1) * 100 / shardTotal));
                fileReader.onload = function (e) {
                    let base64 = e.target.result;
                    // console.log("base64:", base64);

                    param.shard = base64;

                    _this.$ajax.post(process.env.VUE_APP_SERVER + '/file/admin/upload', param).then((response) => {
                        let resp = response.data;
                        console.log("上传文件成功:", resp);
                        Progress.show(parseInt(shardIndex * 100 / shardTotal));
                        if (shardIndex < shardTotal) {
                            // 上传下一个分片
                            param.shardIndex = param.shardIndex + 1;
                            _this.upload(param);
                        } else {
                            Progress.hide();
                            _this.afterUpload(resp);
                            $("#" + _this.inputId + "-input").val("");
                        }
                    });
                };
                fileReader.readAsDataURL(fileShard);
            },

常见问题

能不能给我介绍一下MultipartFile

答: 这个是Spring框架自带的一个类,便于用户更好操作网络传输的文件,这个类为我们提供了很多便捷操作的API。

getName():获取文件名
getOriginalFilename():返回客户端系统中原始文件名。
getContentType():获取文件内容类型。
isEmpty():判断文件是否为空。
getSize():获取文件大小,以字节为单位。
getBytes():获取文件的字节数组。
getInputStream():获取文件输入流。
transferTo(File dest):将目标文件传输到目标文件中。
transferTo(Path dest) :将目标文件传输到目标地址中。

以笔者为例,笔者为了将前端传入的base64字符串转为MultipartFile,于是继承了MultipartFile编写了一个工具类

这个类首先声明两个成员属性,imgContent记录文件内容,header记录base64文件标识。

 private final byte[] imgContent;
 private final String header;

获取文件名以及获取文件类型等相关方法的重写

 @Override
    public String getName() {
        // TODO - implementation depends on your requirements
        return System.currentTimeMillis() + Math.random() + "." + header.split("/")[1];
    }

    @Override
    public String getOriginalFilename() {
        // TODO - implementation depends on your requirements
        return System.currentTimeMillis() + (int) Math.random() * 10000 + "." + header.split("/")[1];
    }
@Override
    public String getContentType() {
        // TODO - implementation depends on your requirements
        return header.split(":")[1];
    }

重点:base64MultipartFile 逻辑,如下所示,将base64封号后面的内容转为数组b ,然后将这个数据赋给imgContent,文件描述信息赋给header

public static MultipartFile base64ToMultipart(String base64) {
        try {
            String[] baseStrs = base64.split(",");

            BASE64Decoder decoder = new BASE64Decoder();
            byte[] b = null;
            b = decoder.decodeBuffer(baseStrs[1]);

            for(int i = 0; i < b.length; ++i) {
                if (b[i] < 0) {
                    b[i] += 256;
                }
            }

			//将文件内容赋值给imgContent,文件描述信息baseStrs[0]赋给header
            return new Base64ToMultipartFile(b, baseStrs[0]);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

 public Base64ToMultipartFile(byte[] imgContent, String header) {
        this.imgContent = imgContent;
        this.header = header.split(";")[0];
    }

base64又是什么类型的数据

答: base64是为了在网络传输中避免中文乱码、媒体文件二进制传输数据不可打印等情况诞生的一种编码技术。它的编码过程也很简单,就是将每个字符串的二进制编码值全部合在一起,每6位算一个字符,再配合base64编码表得出最终结果。

base64编码表

在这里插入图片描述

我们就以字符串A为例子,它的ASCII码值为65,那么二进制数值就为:1000001,按照base64的规则,将字符串中24位为一组(不足24位则算做空位)。所以A的二进制值为01 00 00 01 00 00 后12位全空,每6位算出一个十进制的值,根据base64编码表得出最终的值,01 00 00 得到一个Q,01 00 00又得到一个Q,后面全空的6位算一个=,最终结果为QQ==

在这里插入图片描述

为了让读者加深对base64编码的理解,我们再以字符串Man为例,我们得出它的ASCII码值为77 97 110

它的计算过程如下图所示

在这里插入图片描述

对此我们可以用Java代码来验证一下

 public static void main(String[] args) {
        String man = "Man";
        String a = "A";

        BASE64Encoder encoder = new BASE64Encoder();
        System.out.println("Man base64结果为:" + encoder.encode(man.getBytes()));
        System.out.println("A base64结果为:" + encoder.encode(a.getBytes()));
    }

输出结果

Man base64结果为:TWFu
A base64结果为:QQ==

能不能基于上述接口给我会绘制一张流程图

在这里插入图片描述

关于文件分片上传有没有考虑过OSS

答: 有的,某些场景下我们的项目会使用Amazon S3,相比自建文件服务器,减少了很多运维压力,而且Amazon S3也为我们提供了multipart upload API,最大支持上传5TB的文件。
为了保证扩展,我们也留了一个策略类实现Amazon S3的文件上传。

给我说说你们文件上传的场景吧

答: 我们某个系统需要调用另一个服务的RPC接口进行文件上传,由于某些文件属于大文件,所以经常出现文件上传途中导致文件超时等问题,对此我们采用了文件分片上传的方案解决问题超时问题。

你觉得你这个设计还有没有提高的空间

  1. 输入输出流可以加一个buffer缓冲区。
  2. 如果上传的文件并不是第一时刻要用的话,合并逻辑可以异步执行,即加个Async注解。
  3. 分片大小参数化,根据生产环境服务器性能进行动态调整。

如果需要进行并发上传的话

可以的,并发的逻辑交给前端实现,但是后端的表结构可能需要修改,还记得我们之前有一张专门记录文件分片上传进度的数据表嘛,建表SQL如下所示:

DROP TABLE IF EXISTS `file`;
CREATE TABLE `file` (
  `id` char(8) NOT NULL DEFAULT '' COMMENT 'id',
  `path` varchar(100) NOT NULL COMMENT '相对路径',
  `name` varchar(100) DEFAULT NULL COMMENT '文件名',
  `suffix` varchar(10) DEFAULT NULL COMMENT '后缀',
  `size` int(11) DEFAULT NULL COMMENT '大小|字节B',
  `use` char(1) DEFAULT NULL COMMENT '用途|枚举[FileUseEnum]:COURSE("C", "讲师"), TEACHER("T", "课程")',
  `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
  `updated_at` datetime(3) DEFAULT NULL COMMENT '修改时间',
  `shard_index` int(11) DEFAULT NULL COMMENT '已上传分片',
  `shard_size` int(11) DEFAULT NULL COMMENT '分片大小|B',
  `shard_total` int(11) DEFAULT NULL COMMENT '分片总数',
  `key` varchar(32) DEFAULT NULL COMMENT '文件标识',
  PRIMARY KEY (`id`),
  UNIQUE KEY `path_unique` (`path`),
  UNIQUE KEY `key_unique` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件';

如果需要并发shard_index就不能代表当前已上传的分片数了,取而代之的是我们必须为这个文件上传完成的每一个分片进行日志记录。所以我们可以建立下面这样一张表。

可以看到我们用file表的id和这张表进行关联,每一个分片上传完成后就将分片索引号插入到这张表中。

DROP TABLE IF EXISTS `file_shard`;
CREATE TABLE `file_shard` (
  `id` char(8) NOT NULL DEFAULT '' COMMENT 'id',
  `shard_index` int(11) DEFAULT NULL COMMENT '分片索引'
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件分片';

同步的我们合并的逻辑也得修改。

//从file_shard获取已上传完成的分片数
List<Integer> indexList=fileShradServive.selectById(fileDto.getId())

 //如果分片表的数据数和file表的total一样则可以合并
        if (indexList.size().equals(fileDto.getShardTotal())) {
            fileDto.setPath(fileFullPath);
            //文件合并
            merge(fileDto);
        }

断点续传同理,需要遍历file_shard表的分片索引,for循环看看缺哪个分片就把哪个分片返回给前端

List<Integer> indexList=fileShradServive.selectById(fileDto.getId())

//记录未上传的分片

List<Integer> unfinishUploadShardList=new ArrayList<>();
//分片索引排序
 Collections.sort(indexList);

        //记录未上传的分片
        List<Integer> unfinishUploadShardList = new ArrayList<>();

        for (int i = 0; i < indexList.size(); i++) {
            if (!indexList.contains(i)) {
                unfinishUploadShardList.add(i);
            }
        }

参考文献

MultipartFile实现文件上传与下载

一篇文章彻底弄懂Base64编码原理

一文读懂 AWS S3

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java实现文件分片上传 的相关文章

随机推荐

  • sql如何取前几行_sql 取前几行记录语句

    SQLITE数据库 代码如下 select from table limit N db2数据库 代码如下 select from tab fetch first 10 rows only oracle数据库 代码如下 select from
  • 静态测试和动态测试

    1 静态测试 静态测试 static testing 就是不实际运行被测软件 而只是静态地检查程序代码 界面或文档中可能存在的错误的过程 包括对代码测试 界面测试和文档测试三个方面 对于代码测试 主要测试代码是否符合相应的标准和规范 对于界
  • Asahi Linux for M1 Apple Silicon 首次发布 Alpha 版

    Apple Silicon 的基于 Arch 的发行版只能用于更轻松地安装 OpenBSD Asahi Linux已经为Apple M1 M1 Pro或M1 Max设备上的用户发布了其第一个公共alpha版本 该发行版基于Arch Linu
  • 快手开店怎么引流?快手小店自上线以来就吸引众多的商家入驻

    快手开店怎么引流 快手小店自上线以来就吸引众多的商家入驻 快手小店自上线以来就吸引众多的商家入驻 当然也有不少快手主播粉丝多了也会去卖货多赚点钱 在快手上面开店重视的还是流量 如何才能给店铺带来更多的流量呢 商家需要怎么做 下面一起来看快手
  • 程序员模式

    在我的心中 程序员是一个做事有计划 有思想 具有高超技术 解决能力的艺术家 自己作为一个程序员 自愧不能达到如上的标准 看到过一个程序员曾经这样自嘲 一个只有半瓶子水晃晃荡荡的程序员 这些年来一直从事开发的工作 稀里糊涂跑过许多城市 流浪过
  • Oracle环境变量配置步骤

    Oracle11g环境变量配置 在做开发的过程中 几次重装系统安装配置过Oracle 本篇博客就对oracle配置环境变量的细节做一次记录和分享 三个模块 Oracle11g的安装 instantclient 11 2客户端的安装 PLSQ
  • waiting for ZeroTier system service,

    查了好几个回答 waiting for ZeroTier system service这个错误是之前装过但是卸载后未删除干净造成的 我是卸载后删除了下边四个路径下的Zero Tier文件夹 然后重装就好了 Program Files Pro
  • Elasticsearch7.17 四 : ElasticSearch集群架构

    文章目录 ElasticSearch集群架构 核心概念 节点 分片 Primary Shard Replica Shard 集群状态和分片设定 集群搭建 安装Cerebro客户端 安装kibana ES安全认证 集群内部安全通信 开启并配置
  • Wireshark应用

    1 过滤IP 如来源IP或者目标IP等于某个IP 例子 ip src eq 192 168 1 107 or ip dst eq 192 168 1 107 或者 ip addr eq 192 168 1 107 都能显示来源IP和目标IP
  • ecshop缓存清理-限制或禁用ECShop缓存

    ECSHOP的缓存存放在templates caches 文章夹下 时间长了这个文件夹就会非常庞大 拖慢网站速度 还有很多情况我们不需要他的缓存 本文介绍禁用ECSHOP缓存的方法 ECSHOP的缓存有两部分 一部分是SMARTY的页面缓存
  • Unity 获取UI(RectTransform)四个角的屏幕坐标

    获取UI RectTransform 四个角的屏幕坐标 Vector3 corners new Vector3 4
  • oj2016: C语言实验——打印金字塔

    问题描述 输入n值 打印下列形状的金字塔 其中n代表金字塔的层数 作者 何知令 发表时间 2017年2月23日4 输入 输入只有一个正整数n 输出 打印金字塔图形 其中每个数字之间有一个空格 代码 问题描述 输入n值 打印下列形状的金字塔
  • 小m序列的verilog实现

    verilog实现及仿真 m sequence v 以x8 x4 x3 x2 1为例 module m sequence input sclk input rst n output wire m seq parameter POLY 8 b
  • 【每日一题】补档 ABC309F - Box in Box

    题目内容 原题链接 给定 n n n 个箱子 问是否存在一个箱子 x x x 是否可以放到另一个箱子 y
  • Java简单实现斗地主洗牌、发牌功能

    需求 在启动游戏房间的时候 应该提前准备好54张牌 完成洗牌 发牌 牌排序 逻辑 分析 当系统启动的同时需要准备好数据的时候 就可以用静态代码块了 洗牌就是打乱牌的顺序 定义三个玩家 依次发出51张牌 给玩家的牌进行排序 拓展 输出每个玩家
  • 如何编写测试用例?流程及5大编写步骤

    编写测试用例的5个步骤 1 选择测试工具 2 确定测试场景 3 编写测试用例 4 确认测试用例 5 组织测试用例 但在编写测试用例之前 测试人员需要充分了解软件的需求和规格 以确保测试用例能够覆盖所有的功能和场景 测试用例是一种用于验证软件
  • python 【组成最大数】

    组成最大数 小组中每位都有一张卡片 卡片上是6位内的正整数 将卡片连起来可以组成多种数字 计算组成的最大数字 输入描述 号分割的多个正整数字符串 不需要考虑非数字异常情况 小组最多25个人 输出描述 最大的数字字符串 示例1 输入 22 2
  • 2023年武汉市中等职业学校技能大赛 “网络搭建与应用”

    2023年武汉市中等职业学校技能大赛 网络搭建与应用 一 竞赛内容分布 网络搭建及应用 竞赛共分二个部分 其中 第一部分 企业网络搭建部署项目 占总分的比例为50 第二部分 企业网络服务配置及应用项目 占总分的比例为50 项目背景及网络拓扑
  • OpenCV函数cvWaitKey(k)简介

    作者本人的开发环境为VS的MFC构架 结合OpenCV1 0进行图像的处理 可能很多像作者本人一样的初始开发程序员都会用到cvWaitKey 但是对cvWaitKey 的理解一知半解 在具体开发中会由此产生一些困惑 在查询了一些资料后 将资
  • Java实现文件分片上传

    为什么需要文件分片上传 大文件上传中断 假如我们有一个5G的文件 上传过程中突然中断我们该怎么办 上文件上传响应时间长 假如我们有个10G的文件 单次上传时间长 用户体验长 该怎么办 大文件上传重复上传 某些大文件 我们已经上传过了 我们不