【SpringBoot整合系列】SpringBoot 实现大文件分片上传、断点续传及秒传

目录

  • 功能介绍
    • 文件上传
    • 分片上传
    • 秒传
    • 断点续传
  • 相关概念
  • 相关方法
  • 大文件上传流程
    • 前端切片处理逻辑
    • 后端处理切片的逻辑
    • 流程解析
  • 后端代码实现
    • 功能目标
    • 1.建表SQL
    • 2.引入依赖
    • 3.实体类
    • 4.响应模板
    • 5.枚举类
    • 6.自定义异常
    • 7.工具类
    • 8.Controller层
    • 9.FileService
    • 10.LocalStorageService
    • 11.FileChunkService
    • 12. Repository
    • 13.跨域配置
  • 前端Vue
    • 源码下载地址:
    • 关键代码
      • 安装插件、指定分片大小
    • 定义后端接口地址、判断分片是否上传
      • 计算MD5,并校验是否已上传
      • 计算上传进度

功能介绍

文件上传

  • 小文件(图片、文档、视频)上传可以直接使用很多ui框架封装的上传组件,或者自己写一个input 上传,利用FormData 对象提交文件数据,后端使用spring提供的MultipartFile进行文件的接收,然后写入即可。
  • 但是对于比较大的文件,比如上传2G左右的文件(http上传),就需要将文件分片上传(file.slice()),否则中间http长时间连接可能会断掉

分片上传

分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件

秒传

  • 通俗的说,你把要上传的东西上传,服务器会先做MD5校验,如果服务器上有一样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件
  • 想要不秒传,其实只要让MD5改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5就变了,就不会秒传了

断点续传

  • 断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载
  • 如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。本文的断点续传主要是针对断点上传场景。

相关概念

  • chunkNumber: 当前块的次序,第一个块是 1,注意不是从 0 开始的。
  • totalChunks: 文件被分成块的总数。
  • chunkSize: 分块大小,根据 totalSize 和这个值你就可以计算出总共的块数。注意最后一块的大小可能会比这个要大。
  • currentChunkSize: 当前块的大小,实际大小。
  • totalSize: 文件总大小。
  • identifier: 这个就是MD5值,每个文件的唯一标示。
  • filename: 文件名

相关方法

  • .upload() 开始或者继续上传。
  • .pause() 暂停上传。
  • .resume() 继续上传。
  • .cancel() 取消所有上传文件,文件会被移除掉。
  • .progress() 返回一个0-1的浮点数,当前上传进度。
  • .isUploading() 返回一个布尔值标示是否还有文件正在上传中。
  • .addFile(file) 添加一个原生的文件对象到上传列表中。
  • .removeFile(file) 从上传列表中移除一个指定的 Uploader.File 实例对象。

大文件上传流程

  1. 前端对文件进行MD5加密,并且将文件按一定的规则分片
  2. vue-simple-uploader先会发送get请求校验分片数据在服务端是否完整,如果完整则进行秒传,如果不完整或者无数据,则进行分片上传。
  3. 后台校验MD5值,根据上传的序号和分片大小计算相应的开始位置并写入该分片数据到文件中。
    在这里插入图片描述

前端切片处理逻辑

在这里插入图片描述

后端处理切片的逻辑

在这里插入图片描述

流程解析

  1. 在created时,初始化uploader组件,指定分片大小、上传方式等配置。
  2. 在onFileAdded方法中,当选择文件计算MD5后,调用file.resume()开始上传。
  3. file.resume()内部首先发送一个GET请求,询问服务端该文件已上传的分片。
  4. 服务端返回一个JSON,里面包含已上传分片的列表。
  5. uploader组件调用checkChunkUploadedByResponse,校验当前分片是否在已上传的列表中。
  6. 对未上传的分片,file.resume()会继续触发上传该分片的POST请求。
  7. POST请求会包含一个分片的数据和偏移量等信息。
  8. 服务端接收分片数据,写入文件的指定位置并返回成功响应。
  9. uploader组件会记录该分片已上传完成。
  10. 依次上传完所有分片后,服务器端合并所有分片成一个完整的文件。
  11. onFileSuccess被调用,通知上传成功。
  12. 这样通过GET请求询问已上传分片+POST上传未完成分片+校验的方式,实现了断点续传/分片上传。
    在这里插入图片描述

后端代码实现

SpringBoot2.7.16+MySQL+JPA+hutool

功能目标

  1. get请求接口校验上传文件MD5值和文件是否完整
  2. post请求接收上传文件,并且计算分片,写入合成文件
  3. 文件完整上传完成时,往文件存储表tool_local_storage中加一条该文件的信息
  4. get请求接口实现简单的文件下载

1.建表SQL

DROP TABLE IF EXISTS `file_chunk`;
CREATE TABLE `file_chunk`  (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`file_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件名',
`chunk_number` int(11) NULL DEFAULT NULL COMMENT '当前分片,从1开始',
`chunk_size` float NULL DEFAULT NULL COMMENT '分片大小',
`current_chunk_size` float NULL DEFAULT NULL COMMENT '当前分片大小',
`total_size` double(20, 0) NULL DEFAULT NULL COMMENT '文件总大小',
`total_chunk` int(11) NULL DEFAULT NULL COMMENT '总分片数',
`identifier` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件标识',
`relative_path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'md5校验码',
`createtime` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updatetime` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0),
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1529 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `tool_local_storage`;
CREATE TABLE `tool_local_storage`  (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`real_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件真实的名称',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件名',
`suffix` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '后缀',
`path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '路径',
`type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '类型',
`size` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '大小',
`identifier` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'md5校验码\r\n',
`create_by` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建者',
`update_by` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新者',
`createtime` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updatetime` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0),
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3360 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '文件存储' ROW_FORMAT = Compact;

2.引入依赖

		<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.4</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.70</version>
        </dependency>

3.实体类

package com.zjl.domin;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
@Data
@Entity
@Table(name = "file_chunk")
public class FileChunkParam implements Serializable {
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "chunk_number")
    private Integer chunkNumber;

    @Column(name = "chunk_size")
    private Float chunkSize;

    @Column(name = "current_chunk_size")
    private Float currentChunkSize;

    @Column(name = "total_chunk")
    private Integer totalChunks;

    @Column(name = "total_size")
    private Double totalSize;

    @Column(name = "identifier")
    private String identifier;

    @Column(name = "file_name")
    private String filename;

    @Column(name = "relative_path")
    private String relativePath;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @Column(name = "createtime")
    private Date createtime;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @Column(name = "updatetime")
    private Date updatetime;

    @Transient
    private MultipartFile file;
}
package com.zjl.domin;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
@Data
@Entity
@Table(name = "tool_local_storage")
public class LocalStorage implements Serializable {

    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "real_name")
    private String realName;

    @Column(name = "name")
    private String name;

    @Column(name = "suffix")
    private String suffix;

    @Column(name = "path")
    private String path;

    @Column(name = "type")
    private String type;

    @Column(name = "size")
    private String size;

    @Column(name = "identifier")
    private String identifier;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @Column(name = "createtime")
    private Date createtime;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @Column(name = "updatetime")
    private Date updatetime;

    public LocalStorage() {
    }

    public LocalStorage(String realName, String name, String suffix, String path, String type, String size, String identifier) {
        this.realName = realName;
        this.name = name;
        this.suffix = suffix;
        this.path = path;
        this.type = type;
        this.size = size;
        this.identifier = identifier;
    }

    public LocalStorage(Long id, String realName, String name, String suffix, String path, String type, String size, String identifier) {
        this.id = id;
        this.realName = realName;
        this.name = name;
        this.suffix = suffix;
        this.path = path;
        this.type = type;
        this.size = size;
        this.identifier = identifier;
    }

    public void copy(LocalStorage source) {
        BeanUtil.copyProperties(source, this, CopyOptions.create().setIgnoreNullValue(true));
    }
}

4.响应模板

package com.zjl.domin;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
@Data
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ResultVO<T> {
    /**
     * 错误码.
     */
    private Integer code;

    /**
     * 提示信息.
     */
    private String msg;

    /**
     * 具体内容.
     */
    private T data;

    public ResultVO(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public ResultVO(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public ResultVO() {
    }
}

5.枚举类

package com.zjl.enums;

import lombok.Getter;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
public enum MessageEnum {
    /**
     * 消息枚举
     */
    FAIL(-1, "操作失败"),
    SUCCESS(200, "操作成功"),
    RECORD_NOT_EXISTED(1001, "记录不存在"),
    PARAM_NOT_NULL(1002, "参数不能为空"),
    PARAM_INVALID(1003, "参数错误"),
    UPLOAD_FILE_NOT_NULL(1004, "上传文件不能为空"),
    OVER_FILE_MAX_SIZE(1005, "超出文件大小");

    MessageEnum(int value, String text) {
        this.code = value;
        this.message = text;
    }

    @Getter
    private final int code;

    @Getter
    private final String message;

    public static MessageEnum valueOf(int value) {
        MessageEnum[] enums = values();
        for (MessageEnum enumItem : enums) {
            if (value == enumItem.getCode()) {
                return enumItem;
            }
        }
        return null;
    }
}

6.自定义异常

package com.zjl.exception;

import com.zjl.enums.MessageEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public abstract class BaseErrorException extends RuntimeException {

    private static final long serialVersionUID = 6386720492655133851L;
    private int code;
    private String error;

    public BaseErrorException(MessageEnum messageEnum) {
        this.code = messageEnum.getCode();
        this.error = messageEnum.getMessage();
    }
}
package com.zjl.exception;

import com.zjl.enums.MessageEnum;
import lombok.Data;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
@Data
public class BusinessException extends BaseErrorException {

    private static final long serialVersionUID = 2369773524406947262L;

    public BusinessException(MessageEnum messageEnum) {
        super(messageEnum);
    }

    public BusinessException(String error) {
        super.setCode(-1);
        super.setError(error);
    }
}

7.工具类

package com.zjl.utils;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.poi.excel.BigExcelWriter;
import cn.hutool.poi.excel.ExcelUtil;
import com.zjl.enums.MessageEnum;
import com.zjl.exception.BusinessException;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import sun.misc.BASE64Encoder;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:File工具类,扩展 hutool 工具包
 */
public class FileUtil extends cn.hutool.core.io.FileUtil {
    private static final Logger log = LoggerFactory.getLogger(FileUtil.class);
    /**
     * 系统临时目录
     * <br>
     * windows 包含路径分割符,但Linux 不包含,
     * 在windows \\==\ 前提下,
     * 为安全起见 同意拼装 路径分割符,
     * <pre>
     *       java.io.tmpdir
     *       windows : C:\Users/xxx\AppData\Local\Temp\
     *       linux: /temp
     * </pre>
     */
    public static final String SYS_TEM_DIR = System.getProperty("java.io.tmpdir") + File.separator;
    /**
     * 定义GB的计算常量
     */
    private static final int GB = 1024 * 1024 * 1024;
    /**
     * 定义MB的计算常量
     */
    private static final int MB = 1024 * 1024;
    /**
     * 定义KB的计算常量
     */
    private static final int KB = 1024;

    /**
     * 格式化小数
     */
    private static final DecimalFormat DF = new DecimalFormat("0.00");

    /**
     * MultipartFile转File
     */
    public static File toFile(MultipartFile multipartFile) {
        // 获取文件名
        String fileName = multipartFile.getOriginalFilename();
        // 获取文件后缀
        String prefix = "." + getExtensionName(fileName);
        File file = null;
        try {
            // 用uuid作为文件名,防止生成的临时文件重复
            file = File.createTempFile(IdUtil.simpleUUID(), prefix);
            // MultipartFile to File
            multipartFile.transferTo(file);
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
        return file;
    }

    /**
     * 获取文件扩展名,不带 .
     */
    public static String getExtensionName(String filename) {
        if ((filename != null) && (filename.length() > 0)) {
            int dot = filename.lastIndexOf('.');
            if ((dot > -1) && (dot < (filename.length() - 1))) {
                return filename.substring(dot + 1);
            }
        }
        return filename;
    }

    /**
     * Java文件操作 获取不带扩展名的文件名
     */
    public static String getFileNameNoEx(String filename) {
        if ((filename != null) && (filename.length() > 0)) {
            int dot = filename.lastIndexOf('.');
            if ((dot > -1) && (dot < (filename.length()))) {
                return filename.substring(0, dot);
            }
        }
        return filename;
    }

    /**
     * 文件大小转换
     */
    public static String getSize(long size) {
        String resultSize;
        if (size / GB >= 1) {
            //如果当前Byte的值大于等于1GB
            resultSize = DF.format(size / (float) GB) + "GB   ";
        } else if (size / MB >= 1) {
            //如果当前Byte的值大于等于1MB
            resultSize = DF.format(size / (float) MB) + "MB   ";
        } else if (size / KB >= 1) {
            //如果当前Byte的值大于等于1KB
            resultSize = DF.format(size / (float) KB) + "KB   ";
        } else {
            resultSize = size + "B   ";
        }
        return resultSize;
    }

    /**
     * inputStream 转 File
     */
    static File inputStreamToFile(InputStream ins, String name) throws Exception {
        File file = new File(SYS_TEM_DIR + name);
        if (file.exists()) {
            return file;
        }
        OutputStream os = new FileOutputStream(file);
        int bytesRead;
        int len = 8192;
        byte[] buffer = new byte[len];
        while ((bytesRead = ins.read(buffer, 0, len)) != -1) {
            os.write(buffer, 0, bytesRead);
        }
        os.close();
        ins.close();
        return file;
    }

    /**
     * 将文件名解析成文件的上传路径
     */
    public static File upload(MultipartFile file, String filePath) {
        Date date = new Date();
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddhhmmssS");
        String name = getFileNameNoEx(file.getOriginalFilename());
        String suffix = getExtensionName(file.getOriginalFilename());
        String nowStr = "-" + format.format(date);
        try {
            String fileName = name + nowStr + "." + suffix;
            String path = filePath + fileName;
            // getCanonicalFile 可解析正确各种路径
            File dest = new File(path).getCanonicalFile();
            // 检测是否存在目录
            if (!dest.getParentFile().exists()) {
                if (!dest.getParentFile().mkdirs()) {
                    System.out.println("was not successful.");
                }
            }
            // 文件写入
            file.transferTo(dest);
            return dest;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return null;
    }

    /**
     * 导出excel
     */
    public static void downloadExcel(List<Map<String, Object>> list, HttpServletResponse response) throws IOException {
        String tempPath = SYS_TEM_DIR + IdUtil.fastSimpleUUID() + ".xlsx";
        File file = new File(tempPath);
        BigExcelWriter writer = ExcelUtil.getBigWriter(file);
        // 一次性写出内容,使用默认样式,强制输出标题
        writer.write(list, true);
        //response为HttpServletResponse对象
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        //test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码
        response.setHeader("Content-Disposition", "attachment;filename=file.xlsx");
        ServletOutputStream out = response.getOutputStream();
        // 终止后删除临时文件
        file.deleteOnExit();
        writer.flush(out, true);
        //此处记得关闭输出Servlet流
        IoUtil.close(out);
    }

    public static String getFileType(String type) {
        String documents = "txt pdf pps wps doc docx ppt pptx xls xlsx";
        String music = "mp3 wav wma mpa ram ra aac aif m4a";
        String video = "avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg";
        String image = "bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg";
        if (image.contains(type)) {
            return "图片";
        } else if (documents.contains(type)) {
            return "文档";
        } else if (music.contains(type)) {
            return "音乐";
        } else if (video.contains(type)) {
            return "视频";
        } else {
            return "其他";
        }
    }

    public static String getTransferFileType(String type) {
        String documents = "txt pdf pps wps doc docx ppt pptx xls xlsx";
        String music = "mp3 wav wma mpa ram ra aac aif m4a";
        String video = "avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg";
        String image = "bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg";
        if (image.contains(type)) {
            return "image";
        } else if (documents.contains(type)) {
            return "documents";
        } else if (music.contains(type)) {
            return "music";
        } else if (video.contains(type)) {
            return "video";
        } else {
            return "other";
        }
    }

    public static void checkSize(long maxSize, long size) {
        // 1M
        int len = 1024 * 1024;
        if (size > (maxSize * len)) {
            throw new BusinessException(MessageEnum.OVER_FILE_MAX_SIZE);
        }
    }

    /**
     * 判断两个文件是否相同
     */
    public static boolean check(File file1, File file2) {
        String img1Md5 = getMd5(file1);
        String img2Md5 = getMd5(file2);
        return img1Md5.equals(img2Md5);
    }

    /**
     * 判断两个文件是否相同
     */
    public static boolean check(String file1Md5, String file2Md5) {
        return file1Md5.equals(file2Md5);
    }

    private static byte[] getByte(File file) {
        // 得到文件长度
        byte[] b = new byte[(int) file.length()];
        try {
            InputStream in = new FileInputStream(file);
            try {
                System.out.println(in.read(b));
            } catch (IOException e) {
                log.error(e.getMessage(), e);
            }
        } catch (FileNotFoundException e) {
            log.error(e.getMessage(), e);
            return null;
        }
        return b;
    }

    private static String getMd5(byte[] bytes) {
        // 16进制字符
        char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
        try {
            MessageDigest mdTemp = MessageDigest.getInstance("MD5");
            mdTemp.update(bytes);
            byte[] md = mdTemp.digest();
            int j = md.length;
            char[] str = new char[j * 2];
            int k = 0;
            // 移位 输出字符串
            for (byte byte0 : md) {
                str[k++] = hexDigits[byte0 >>> 4 & 0xf];
                str[k++] = hexDigits[byte0 & 0xf];
            }
            return new String(str);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return null;
    }

    /**
     * 下载文件
     *
     * @param request  /
     * @param response /
     * @param file     /
     */
    public static void downloadFile(HttpServletRequest request, HttpServletResponse response, File file, boolean deleteOnExit) throws UnsupportedEncodingException {
        response.setCharacterEncoding(request.getCharacterEncoding());
        response.setContentType("application/octet-stream");
        FileInputStream fis = null;

        String filename = filenameEncoding(file.getName(), request);
        try {
            fis = new FileInputStream(file);
            response.setHeader("Content-Disposition", String.format("attachment;filename=%s", filename));
            IOUtils.copy(fis, response.getOutputStream());
            response.flushBuffer();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                    if (deleteOnExit) {
                        file.deleteOnExit();
                    }
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                }
            }
        }
    }

    public static String getMd5(File file) {
        return getMd5(getByte(file));
    }

    public static String filenameEncoding(String filename, HttpServletRequest request) throws UnsupportedEncodingException {
        // 获得请求头中的User-Agent
        String agent = request.getHeader("User-Agent");
        // 根据不同的客户端进行不同的编码

        if (agent.contains("MSIE")) {
            // IE浏览器
            filename = URLEncoder.encode(filename, "utf-8");
        } else if (agent.contains("Firefox")) {
            // 火狐浏览器
            BASE64Encoder base64Encoder = new BASE64Encoder();
            filename = "=?utf-8?B?" + base64Encoder.encode(filename.getBytes("utf-8")) + "?=";
        } else {
            // 其它浏览器
            filename = URLEncoder.encode(filename, "utf-8");
        }
        return filename;
    }
}

8.Controller层

package com.zjl.controller;
import com.zjl.domin.FileChunkParam;
import com.zjl.domin.ResultVO;
import com.zjl.service.FileChunkService;
import com.zjl.service.FileService;
import com.zjl.service.LocalStorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
@RestController
@Slf4j
@RequestMapping("/api")
public class FileUploadController {

    @Resource
    private FileService fileService;
    @Resource
    private FileChunkService fileChunkService;
    @Resource
    private LocalStorageService localStorageService;

    @GetMapping("/upload")
    public ResultVO<Map<String, Object>> checkUpload(FileChunkParam param) {
        log.info("文件MD5:" + param.getIdentifier());
        List<FileChunkParam> list = fileChunkService.findByMd5(param.getIdentifier());
        Map<String, Object> data = new HashMap<>(1);
        // 判断文件存不存在
        if (list.size() == 0) {
            data.put("uploaded", false);
            return new ResultVO<>(200, "上传成功", data);
        }
        // 处理单文件
        if (list.get(0).getTotalChunks() == 1) {
            data.put("uploaded", true);
            data.put("url", "");
            return new ResultVO<Map<String, Object>>(200, "上传成功", data);
        }
        // 处理分片
        int[] uploadedFiles = new int[list.size()];
        int index = 0;
        for (FileChunkParam fileChunkItem : list) {
            uploadedFiles[index] = fileChunkItem.getChunkNumber();
            index++;
        }
        data.put("uploadedChunks", uploadedFiles);
        return new ResultVO<Map<String, Object>>(200, "上传成功", data);
    }

    @PostMapping("/upload")
    public ResultVO chunkUpload(FileChunkParam param) {
        log.info("上传文件:{}", param);
        boolean flag = fileService.uploadFile(param);
        if (!flag) {
            return new ResultVO(211, "上传失败");
        }
        return new ResultVO(200, "上传成功");
    }


    @GetMapping(value = "/download/{md5}/{name}")
    public void downloadbyname(HttpServletRequest request, HttpServletResponse response, @PathVariable String name, @PathVariable String md5) throws IOException {
        localStorageService.downloadByName(name, md5, request, response);
    }
}

9.FileService

package com.zjl.service;


import com.zjl.domin.FileChunkParam;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
public interface FileService {
    /**
     * 上传文件
     * @param param 参数
     * @return
     */
    boolean uploadFile(FileChunkParam param);
}
package com.zjl.service.impl;

import com.zjl.domin.FileChunkParam;
import com.zjl.enums.MessageEnum;
import com.zjl.exception.BusinessException;
import com.zjl.service.FileChunkService;
import com.zjl.service.FileService;
import com.zjl.service.LocalStorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import sun.misc.Cleaner;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Method;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.security.AccessController;
import java.security.PrivilegedAction;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
@Service("fileService")
@Slf4j
public class FileServiceImpl implements FileService {
    /**
     * 默认的分片大小:20MB
     */
    public static final long DEFAULT_CHUNK_SIZE = 20 * 1024 * 1024;

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

    @Resource
    private FileChunkService fileChunkService;

    @Resource
    private LocalStorageService localStorageService;

    @Override
    public boolean uploadFile(FileChunkParam param) {
        if (null == param.getFile()) {
            throw new BusinessException(MessageEnum.UPLOAD_FILE_NOT_NULL);
        }
        // 判断目录是否存在,不存在则创建目录
        File savePath = new File(BASE_FILE_SAVE_PATH);
        if (!savePath.exists()) {
            boolean flag = savePath.mkdirs();
            if (!flag) {
                log.error("保存目录创建失败");
                return false;
            }
        }


        //  todo 处理文件夹上传(上传目录下新建上传的文件夹)
        /*String relativePath = param.getRelativePath();
        if (relativePath.contains("/") || relativePath.contains(File.separator)) {
            String div = relativePath.contains(File.separator) ? File.separator : "/";
            String tempPath = relativePath.substring(0, relativePath.lastIndexOf(div));
            savePath = new File(BASE_FILE_SAVE_PATH + File.separator + tempPath);
            if (!savePath.exists()) {
                boolean flag = savePath.mkdirs();
                if (!flag) {
                    log.error("保存目录创建失败");
                    return false;
                }
            }
        }*/


        // 这里可以使用 uuid 来指定文件名,上传完成后再重命名,File.separator指文件目录分割符,win上的"\",Linux上的"/"。
        String fullFileName = savePath + File.separator + param.getFilename();
        // 单文件上传
        if (param.getTotalChunks() == 1) {
            return uploadSingleFile(fullFileName, param);
        }
        // 分片上传,这里使用 uploadFileByRandomAccessFile 方法,也可以使用 uploadFileByMappedByteBuffer 方法上传
        boolean flag = uploadFileByRandomAccessFile(fullFileName, param);
        if (!flag) {
            return false;
        }
        // 保存分片上传信息
        fileChunkService.saveFileChunk(param);
        return true;
    }


    private boolean uploadFileByRandomAccessFile(String resultFileName, FileChunkParam param) {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(resultFileName, "rw")) {
            // 分片大小必须和前端匹配,否则上传会导致文件损坏
            long chunkSize = param.getChunkSize() == 0L ? DEFAULT_CHUNK_SIZE : param.getChunkSize().longValue();
            // 偏移量
            long offset = chunkSize * (param.getChunkNumber() - 1);
            // 定位到该分片的偏移量
            randomAccessFile.seek(offset);
            // 写入
            randomAccessFile.write(param.getFile().getBytes());
        } catch (IOException e) {
            log.error("文件上传失败:" + e);
            return false;
        }
        return true;
    }

    private boolean uploadFileByMappedByteBuffer(String resultFileName, FileChunkParam param) {
        // 分片上传
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(resultFileName, "rw");
             FileChannel fileChannel = randomAccessFile.getChannel()) {
            // 分片大小必须和前端匹配,否则上传会导致文件损坏
            long chunkSize = param.getChunkSize() == 0L ? DEFAULT_CHUNK_SIZE : param.getChunkSize().longValue();
            // 写入文件
            long offset = chunkSize * (param.getChunkNumber() - 1);
            byte[] fileBytes = param.getFile().getBytes();
            MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileBytes.length);
            mappedByteBuffer.put(fileBytes);
            // 释放
            unmap(mappedByteBuffer);
        } catch (IOException e) {
            log.error("文件上传失败:" + e);
            return false;
        }
        return true;
    }

    private boolean uploadSingleFile(String resultFileName, FileChunkParam param) {
        File saveFile = new File(resultFileName);
        try {
            // 写入
            param.getFile().transferTo(saveFile);
            localStorageService.saveLocalStorage(param);
        } catch (IOException e) {
            log.error("文件上传失败:" + e);
            return false;
        }
        return true;
    }

    /**
     * 释放 MappedByteBuffer
     * 在 MappedByteBuffer 释放后再对它进行读操作的话就会引发 jvm crash,在并发情况下很容易发生
     * 正在释放时另一个线程正开始读取,于是 crash 就发生了。所以为了系统稳定性释放前一般需要检
     * 查是否还有线程在读或写
     * 来源:https://my.oschina.net/feichexia/blog/212318
     *
     * @param mappedByteBuffer mappedByteBuffer
     */
    public static void unmap(final MappedByteBuffer mappedByteBuffer) {
        try {
            if (mappedByteBuffer == null) {
                return;
            }
            mappedByteBuffer.force();
            AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
                try {
                    Method getCleanerMethod = mappedByteBuffer.getClass()
                            .getMethod("cleaner");
                    getCleanerMethod.setAccessible(true);
                    Cleaner cleaner =
                            (Cleaner) getCleanerMethod
                                    .invoke(mappedByteBuffer, new Object[0]);
                    cleaner.clean();
                } catch (Exception e) {
                    log.error("MappedByteBuffer 释放失败:" + e);
                }
                System.out.println("clean MappedByteBuffer completed");
                return null;
            });
        } catch (Exception e) {
            log.error("unmap error:" + e);
        }
    }
}

10.LocalStorageService

package com.zjl.service;


import com.zjl.domin.FileChunkParam;
import com.zjl.domin.LocalStorage;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
public interface LocalStorageService {
    /**
     * 根据文件 md5 查询
     *
     * @param md5 md5
     * @return
     */
    LocalStorage findByMd5(String md5);

    /**
     * 保存记录
     *
     * @param localStorage 记录参数
     */
    void saveLocalStorage(LocalStorage localStorage);

    /**
     * 保存记录
     *
     * @param param 记录参数
     */
    void saveLocalStorage(FileChunkParam param);

    /**
     * 删除记录
     *
     * @param localStorage localStorage
     * @return
     */
    void delete(LocalStorage localStorage);

    /**
     * 根据 id 删除
     *
     * @param id id
     * @return
     */
    void deleteById(Long id);

    void downloadByName(String name, String md5, HttpServletRequest request, HttpServletResponse response);
}
package com.zjl.service.impl;

import com.zjl.domin.FileChunkParam;
import com.zjl.domin.LocalStorage;
import com.zjl.repository.LocalStorageRepository;
import com.zjl.service.LocalStorageService;
import com.zjl.utils.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.UnsupportedEncodingException;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
@Service
@Slf4j
public class LocalStorageServiceImpl implements LocalStorageService {
    @Resource
    private LocalStorageRepository localStorageRepository;

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

    @Override
    public LocalStorage findByMd5(String md5) {
        return localStorageRepository.findByIdentifier(md5);
    }

    @Override
    public void saveLocalStorage(LocalStorage localStorage) {
        localStorageRepository.save(localStorage);
    }

    @Override
    public void saveLocalStorage(FileChunkParam param) {
        Long id = null;
        LocalStorage byIdentifier = localStorageRepository.findByIdentifier(param.getIdentifier());
        if (!ObjectUtils.isEmpty(byIdentifier)) {
            id = byIdentifier.getId();
        }
        String name = param.getFilename();
        String suffix = FileUtil.getExtensionName(name);
        String type = FileUtil.getFileType(suffix);
        LocalStorage localStorage = new LocalStorage(
                id,
                name,
                FileUtil.getFileNameNoEx(name),
                suffix,
                param.getRelativePath(),
                type,
                FileUtil.getSize(param.getTotalSize().longValue()),
                param.getIdentifier()
        );
        localStorageRepository.save(localStorage);
    }

    @Override
    public void delete(LocalStorage localStorage) {
        localStorageRepository.delete(localStorage);
    }

    @Override
    public void deleteById(Long id) {
        localStorageRepository.deleteById(id);
    }

    @Override
    public void downloadByName(String name, String md5, HttpServletRequest request, HttpServletResponse response) {
        LocalStorage storage = localStorageRepository.findByRealNameAndIdentifier(name, md5);
        if (ObjectUtils.isEmpty(storage)) {
            return;
        }
        File tofile = new File(BASE_FILE_SAVE_PATH + File.separator + storage.getPath());
        try {
            FileUtil.downloadFile(request, response, tofile, false);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }
}

11.FileChunkService

package com.zjl.service;


import com.zjl.domin.FileChunkParam;

import java.util.List;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
public interface FileChunkService {
    /**
     * 根据文件 md5 查询
     *
     * @param md5 md5
     * @return
     */
    List<FileChunkParam> findByMd5(String md5);

    /**
     * 保存记录
     *
     * @param param 记录参数
     */
    void saveFileChunk(FileChunkParam param);

    /**
     * 删除记录
     *
     * @param fileChunk fileChunk
     * @return
     */
    void delete(FileChunkParam fileChunk);

    /**
     * 根据 id 删除
     *
     * @param id id
     * @return
     */
    void deleteById(Long id);
}
package com.zjl.service.impl;

import com.zjl.domin.FileChunkParam;
import com.zjl.repository.FileChunkRepository;
import com.zjl.service.FileChunkService;
import com.zjl.service.LocalStorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
@Service
public class FileChunkServiceImpl implements FileChunkService {
    @Resource
    private FileChunkRepository fileChunkRepository;

    @Resource
    private LocalStorageService localStorageService;

    @Override
    public List<FileChunkParam> findByMd5(String md5) {
        return fileChunkRepository.findByIdentifier(md5);
    }

    @Override
    public void saveFileChunk(FileChunkParam param) {
        fileChunkRepository.save(param);
        // 当文件分片完整上传完成,存一份在LocalStorage表中
        if (param.getChunkNumber().equals(param.getTotalChunks())) {
            localStorageService.saveLocalStorage(param);
        }
    }

    @Override
    public void delete(FileChunkParam fileChunk) {
        fileChunkRepository.delete(fileChunk);
    }

    @Override
    public void deleteById(Long id) {
        fileChunkRepository.deleteById(id);
    }
}

12. Repository

package com.zjl.repository;

import com.zjl.domin.FileChunkParam;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import java.util.List;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
public interface FileChunkRepository extends JpaRepository<FileChunkParam, Long>, JpaSpecificationExecutor<FileChunkParam> {

    List<FileChunkParam> findByIdentifier(String identifier);
}
package com.zjl.repository;

import com.zjl.domin.LocalStorage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
public interface LocalStorageRepository extends JpaRepository<LocalStorage, Long>, JpaSpecificationExecutor<LocalStorage> {
    LocalStorage findByIdentifier(String identifier);

    LocalStorage findByRealNameAndIdentifier(String name, String md5);
}

13.跨域配置

package com.zjl.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author: zjl
 * @datetime: 2024/4/9
 * @desc:
 */
@Configuration
public class GlobalCorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowCredentials(true)
                .allowedHeaders("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("Authorization", "Cache-Control", "Content-Type")
                .maxAge(3600);
    }
}

前端Vue

源码下载地址:

链接:https://pan.baidu.com/s/1KFzWdq-kfOAxMKDaCPCDPQ?pwd=6666 提取码:6666

关键代码

安装插件、指定分片大小

import SparkMD5 from "spark-md5";
const FILE_UPLOAD_ID_KEY = "file_upload_id";
// 分片大小,20MB
const CHUNK_SIZE = 20 * 1024 * 1024;

定义后端接口地址、判断分片是否上传

// 上传地址
        target: "http://127.0.0.1:9999/api/upload",
        // 是否开启服务器分片校验。默认为 true
        testChunks: true,
        // 真正上传的时候使用的 HTTP 方法,默认 POST
        uploadMethod: "post",
        // 分片大小
        chunkSize: CHUNK_SIZE,
        // 并发上传数,默认为 3
        simultaneousUploads: 3,
        /**
         * 判断分片是否上传,秒传和断点续传基于此方法
         * 这里根据实际业务来 用来判断哪些片已经上传过了 不用再重复上传了 [这里可以用来写断点续传!!!]
         */
        checkChunkUploadedByResponse: (chunk, message) => {
          // message是后台返回
          let messageObj = JSON.parse(message);
          let dataObj = messageObj.data;
          if (dataObj.uploaded !== undefined) {
            return dataObj.uploaded;
          }
          // 判断文件或分片是否已上传,已上传返回 true
          // 这里的 uploadedChunks 是后台返回]
          return (dataObj.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0;
        },

计算MD5,并校验是否已上传

onFileAdded(file, event) {
      this.uploadFileList.push(file);
      console.log("file :>> ", file);
      // 有时 fileType为空,需截取字符
      console.log("文件类型:" + file.fileType);
      // 文件大小
      console.log("文件大小:" + file.size + "B");
      // 1. todo 判断文件类型是否允许上传
      // 2. 计算文件 MD5 并请求后台判断是否已上传,是则取消上传
      console.log("校验MD5");
      this.getFileMD5(file, (md5) => {
        if (md5 != "") {
          // 修改文件唯一标识
          file.uniqueIdentifier = md5;
          // 请求后台判断是否上传
          // 恢复上传
          file.resume();
        }
      });
    },
// 计算文件的MD5值
    getFileMD5(file, callback) {
      let spark = new SparkMD5.ArrayBuffer();
      let fileReader = new FileReader();
      //获取文件分片对象(注意它的兼容性,在不同浏览器的写法不同)
      let blobSlice =
        File.prototype.slice ||
        File.prototype.mozSlice ||
        File.prototype.webkitSlice;
      // 当前分片下标
      let currentChunk = 0;
      // 分片总数(向下取整)
      let chunks = Math.ceil(file.size / CHUNK_SIZE);
      // MD5加密开始时间
      let startTime = new Date().getTime();
      // 暂停上传
      file.pause();
      loadNext();
      // fileReader.readAsArrayBuffer操作会触发onload事件
      fileReader.onload = function (e) {
        // console.log("currentChunk :>> ", currentChunk);
        spark.append(e.target.result);
        if (currentChunk < chunks) {
          currentChunk++;
          loadNext();
        } else {
          // 该文件的md5值
          let md5 = spark.end();
          console.log(
            `MD5计算完毕:${md5},耗时:${new Date().getTime() - startTime} ms.`
          );
          // 回调传值md5
          callback(md5);
        }
      };
      fileReader.onerror = function () {
        this.$message.error("文件读取错误");
        file.cancel();
      };
      // 加载下一个分片
      function loadNext() {
        const start = currentChunk * CHUNK_SIZE;
        const end =
          start + CHUNK_SIZE >= file.size ? file.size : start + CHUNK_SIZE;
        // 文件分片操作,读取下一分片(fileReader.readAsArrayBuffer操作会触发onload事件)
        fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
      }
    },
fileStatusText(status, response) {
      if (status === "md5") {
        return "校验MD5";
      } else {
        return this.fileStatusTextObj[status];
      }
    },

计算上传进度

onFileProgress(rootFile, file, chunk) {
      console.log(`当前进度:${Math.ceil(file._prevProgress * 100)}%`);
    },

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

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

相关文章

Mac系统Unity团结引擎打包OpenHomeny项目配置

1、团结引擎下载&#xff1a;直接百度下载即可 2、mac版本的DevEco4.0编辑器下载&#xff1a; widthdevice-width,initial-scale1.0https://docs.openharmony.cn/pages/v4.0/zh-cn/release-notes/OpenHarmony-v4.0-release.md/#%E9%85%8D%E5%A5%97%E5%85%B3%E7%B3%BB3、打开D…

IRIS / Chronicles 基础概念备忘录

数据类型存储不同 在 IRIS 中有 几种数据类型&#xff0c;但是这几种数据类型怎么存的和常用的关系数据库不太一样。 String&#xff1a;字符串类型&#xff0c;这个类型包括有字母和数字&#xff0c;需要注意在这里有一个 Padded 的概念&#xff0c;对与 String 类型的数据&…

深度解析Elasticsearch索引数据量过大的优化与部署策略

✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天开心哦&#xff01;✨✨ &#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; 目录 引言 一. 分片和副本策略 1.1分片策略 1.1.1 数据量 1.1…

spring cloud gateway openfeign 联合使用产生死锁问题

spring cloud gateway openfeign 联合使用产生死锁问题&#xff0c;应用启动的时候阻塞卡住。 spring.cloud 版本如下 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><vers…

春秋之境28512

题目说该CMS的/single.php路径下&#xff0c;id参数存在一个SQL注入漏洞。访问看一下随便点一个图片。 发现了注入点?id 那么开始查看闭合符一个 就报错了 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for th…

【智能算法】孔雀优化算法(POA)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献 1.背景 2022年&#xff0c;Wang等人受到自然界孔雀社会行为启发&#xff0c;提出了孔雀优化算法&#xff08;Peafowl Optimization Algorithm&#xff0c;POA&#xff09;。 2.算法原理 2.1算法思想 POA…

uni-app调用苹果登录,并获取用户信息

效果 模块配置 dev中的配置 需要开启登录的权限&#xff0c;然后重新下载配置文件&#xff0c;发布打包基座&#xff0c;再运行程序 代码 <button click"appleLogin">苹果登录</button>function appleLogin() {uni.login({provider: apple,success: …

硬盘删除的文件怎么恢复?恢复方法大公开!

“硬盘删除的文件还有机会恢复吗&#xff1f;刚刚清理电脑垃圾的时候不小心删除了很多重要的文件&#xff0c;有什么方法可以有效恢复这些文件吗&#xff1f;” 在数据时代&#xff0c;我们会将很多重要的文件都保存在电脑上&#xff0c;如果我们清理了电脑上的文件&#xff0c…

GD32F470_(4线制)火光/火焰传感器模块火源探测 红外接收传感器 智能车配件

2.16 火焰传感器 红外火焰传感器可以用来探测火源或其它一些波长在700纳米~1000纳米范围内的热源&#xff0c;在机器人比赛中&#xff0c;远红外火焰探头起到非常重要的作用&#xff0c;它可以用作机器人的眼睛来寻找火源或足球。利用它可以制作灭火机器人等。 红外火焰传感器…

捷途山海T2探秘武夷山,这款旅行越野超混SUV直接拉满期待值

如今&#xff0c;出行已然不单单是A点到B点的空间位移&#xff0c;而是心灵的一场旅行。每个人都向往在旅途中&#xff0c;寻找到自我的归属。近年走红的捷途汽车&#xff0c;正洞察到用户们的出行迭代需求&#xff0c;推出全新力作捷途山海T2。山海T2是捷途旅行者的混动版&…

Centos7搭建 Skywalking 单机版

介绍 Skywalking是应用性能监控平台&#xff0c;可用于分布式系统&#xff0c;支持微服务、云原生、Docker、Kubernetes 等多种架构场景。 整体架构如图 Agent &#xff1a;在应用中&#xff0c;收集 Trace、Log、Metrics 等监控数据&#xff0c;使用 RPC、RESTful API、Kafk…

七燕论文可靠吗 #经验分享#经验分享

七燕论文是一个非常好用的论文写作工具&#xff0c;它不仅可以帮助学生提高写作效率&#xff0c;还能帮助他们避免抄袭和提高论文质量。七燕论文的查重降重功能非常靠谱&#xff0c;能够帮助用户检测论文中的重复内容&#xff0c;并提供相应的修改建议&#xff0c;确保论文的原…

人大金仓导入数据库

1、通过工具导入 具体步骤如下 逻辑还原选择备份文件还原至数据库还原 2、命令导入 进入如下文件夹下 在本文件夹下打开cmd运行如下命令 sys_restore -h 127.0.0.1 -p 54321 -d test -U system C:/Users/Lenovo/Desktop/lw_2024-01-03_11_11_44.dmp >> C:/Users/Len…

Mysql底层原理四:B+树索引

B树索引&#xff08;索引的原理&#xff09; 1.前言 前边我们详细唠叨了InnoDB数据⻚的7个组成部分&#xff0c;知道了各个数据⻚可以组成⼀个双向链表&#xff0c;⽽每个数据⻚中的记录会按照主键值从⼩到⼤的顺序组成⼀个单向链 表&#xff0c;每个数据⻚都会为存储在它⾥边…

Linux系统磁盘扩容——类型(二)

类型&#xff08;二&#xff09;扩容磁盘到现有逻辑分区目录 查看磁盘空间删除现有逻辑分区再次查看磁盘分区大小更新分区大小 查看磁盘空间 lsblk df -h图中扩容sda磁盘&#xff0c;260G已分配250G&#xff0c;还10G未分配&#xff0c;需要分配到逻辑分区5中。 删除现有逻辑…

基于逻辑回归和支持向量机的前馈网络进行乳腺癌组织病理学图像分类

CNN&#xff08;卷积神经网络&#xff09;通过使用反向传播方法来学习特征&#xff0c;这种方法需要大量的训练数据&#xff0c;并且存在梯度消失问题&#xff0c;从而恶化了特征学习。 CNN卷积神经网络 CNN由一个多层神经网络组成&#xff0c;该网络从标记的训练数据集中学习…

redis 集群模式(redis cluster)介绍

目录 一 redis cluster 相关定义 1&#xff0c; redis cluster 是什么 2&#xff0c;redis 集群的组成 3&#xff0c;集群的作用 4&#xff0c;集群架构图 二 Redis集群的数据分片 1&#xff0c;哈希槽是什么 2&#xff0c;哈希槽如何排布 3&#xff0c;Redis集…

iOS 开发中上传 IPA 文件的方法(无需 Mac 电脑)

引言 在 iOS 开发中&#xff0c;将 IPA 文件上传到苹果开发者中心是一个重要的步骤。通常情况下&#xff0c;我们需要使用 Mac 电脑上的 Xcode 或 Application Loader 工具来完成这个任务。然而&#xff0c;如果你没有 Mac 电脑&#xff0c;也没有关系&#xff0c;本文将介绍一…

天软特色因子看板 (2024.4 第2期)

该因子看板跟踪天软特色因子A05005(近一月单笔流出金额占比(%)&#xff0c;该因子为近一月单笔流出金额占比(% 均值因子&#xff0c;用以刻画下跌时的 单成交中可能存在的抄底现象 今日为该因子跟踪第2期&#xff0c;跟踪其在SH000905 (中证500) 中的表现&#xff0c;要点如下 …

【问题处理】银河麒麟操作系统实例分享,ipelbats转发端口访问ftp目录空白问题处理

1.问题环境 系统环境 物理机 网络环境 私有网络 硬件环境 处理器 arm 软件环境 操作系统版本 V10-SP1-0518-arm 内核版本 4.19.90-23.15.ky10.aarch64 2.问题描述 iptables转发端口访问ftp目录空白&#xff0c;同一个脚本在redhat7.8上正常 2023/2/27&#xff0…