文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Java的分片上传功能的实现

2023-02-14 12:01

关注

起因:最近在工作中接到了一个大文件上传下载的需求,要求将文件上传到share盘中,下载的时候根据前端传的不同条件对单个或多个文件进行打包并设置目录下载。

一开始我想着就还是用老办法直接file.transferTo(newFile)就算是大文件,我只要慢慢等总会传上去的。
(原谅我的无知。。)后来尝试之后发现真的是异想天开了,如果直接用普通的上传方式基本上就会遇到以下4个问题:

所以我只能寻求切片上传的帮助了。

整体思路

前端根据代码中设置好的分片大小将上传的文件切成若干个小文件,分多次请求依次上传,后端再将文件碎片拼接为一个完整的文件,即使某个碎片上传失败,也不会影响其它文件碎片,只需要重新上传失败的部分就可以了。而且多个请求一起发送文件,提高了传输速度的上限。
(前端切片的核心是利用 Blob.prototype.slice 方法,和数组的 slice 方法相似,文件的 slice 方法可以返回原文件的某个切片)

接下来就是上代码!

前端代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- 引入 Vue  -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6/dist/vue.min.js"></script>
    <!-- 引入样式 -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css" rel="external nofollow" >
    <!-- 引入组件库 -->
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    <title>分片上传测试</title>
</head>

<body>
    <div id="app">
        <template>
            <div>
                  <input type="file" @change="handleFileChange" />
                  <el-button @click="handleUpload">上传</el-button>
            </div>
        </template>
    </div>
</body>

</html>
<script>

    // 切片大小
    // the chunk size
    const SIZE = 50 * 1024 * 1024;
    var app = new Vue({
        el: '#app',
        data: {
            container: {
                file: null
            },
            data: [],
            fileListLong: '',
            fileSize:''
        },
        methods: {
            handleFileChange(e) {
                const [file] = e.target.files;
                if (!file) return;
                this.fileSize = file.size;
                Object.assign(this.$data, this.$options.data());
                this.container.file = file;
            },
            async handleUpload() { },
            // 生成文件切片
            createFileChunk(file, size = SIZE) {
                const fileChunkList = [];
                let cur = 0;
                while (cur < file.size) {
                    fileChunkList.push({ file: file.slice(cur, cur + size) });
                    cur += size;
                }
                return fileChunkList;
            },
            // 上传切片
            async uploadChunks() {
                const requestList = this.data
                    .map(({ chunk, hash }) => {
                        const formData = new FormData();
                        formData.append("file", chunk);
                        formData.append("hash", hash);
                        formData.append("filename", this.container.file.name);
                        return { formData };
                    })
                    .map(({ formData }) =>
                        this.request({
                            url: "http://localhost:8080/file/upload",
                            data: formData
                        })
                    );
                // 并发请求
                await Promise.all(requestList);
                console.log(requestList.size);
                this.fileListLong = requestList.length;
                // 合并切片
                await this.mergeRequest();
            },
            async mergeRequest() {
                await this.request({
                    url: "http://localhost:8080/file/merge",
                    headers: {
                        "content-type": "application/json"
                    },
                    data: JSON.stringify({
                        fileSize: this.fileSize,
                        fileNum: this.fileListLong,
                        filename: this.container.file.name
                    })
                });
            },

            async handleUpload() {
                if (!this.container.file) return;
                const fileChunkList = this.createFileChunk(this.container.file);
                this.data = fileChunkList.map(({ file }, index) => ({
                    chunk: file,
                    // 文件名 + 数组下标
                    hash: this.container.file.name + "-" + index
                }));
                await this.uploadChunks();
            },
            request({
                url,
                method = "post",
                data,
                headers = {},
                requestList
            }) {
                return new Promise(resolve => {
                    const xhr = new XMLHttpRequest();
                    xhr.open(method, url);
                    Object.keys(headers).forEach(key =>
                        xhr.setRequestHeader(key, headers[key])
                    );
                    xhr.send(data);
                    xhr.onload = e => {
                        resolve({
                            data: e.target.response
                        });
                    };
                });
            }

        }

    });
</script>

考虑到方便和通用性,这里没有用第三方的请求库,而是用原生 XMLHttpRequest 做一层简单的封装来发请求

当点击上传按钮时,会调用 createFileChunk 将文件切片,切片数量通过文件大小控制,这里设置 50MB,也就是说一个 100 MB 的文件会被分成 2 个 50MB 的切片

createFileChunk 内使用 while 循环和 slice 方法将切片放入 fileChunkList 数组中返回

在生成文件切片时,需要给每个切片一个标识作为 hash,这里暂时使用文件名 + 下标,这样后端可以知道当前切片是第几个切片,用于之后的合并切片

随后调用 uploadChunks 上传所有的文件切片,将文件切片,切片 hash,以及文件名放入 formData 中,再调用上一步的 request 函数返回一个 proimise,最后调用 Promise.all 并发上传所有的切片

后端代码

实体类

@Data
public class FileUploadReq implements Serializable {

    private static final long serialVersionUID = 4248002065970982984L;
    
    //切片的文件
    private MultipartFile file;
    
    //切片的文件名称
    private String hash;
    
    //原文件名称
    private  String filename;
}

@Data
public class FileMergeReq implements Serializable {

    private static final long serialVersionUID = 3667667671957596931L;
    
    //文件名
    private String filename;

    //切片数量
    private int fileNum;

    //文件大小
    private String fileSize;
}
@Slf4j
@CrossOrigin
@RestController
@RequestMapping("/file")
public class FileController {
    final String folderPath = System.getProperty("user.dir") + "/src/main/resources/static/file";

    @RequestMapping(value = "upload", method = RequestMethod.POST)
    public Object upload(FileUploadReq fileUploadEntity) {

        File temporaryFolder = new File(folderPath);
        File temporaryFile = new File(folderPath + "/" + fileUploadEntity.getHash());
        //如果文件夹不存在则创建
        if (!temporaryFolder.exists()) {
            temporaryFolder.mkdirs();
        }
        //如果文件存在则删除
        if (temporaryFile.exists()) {
            temporaryFile.delete();
        }
        MultipartFile file = fileUploadEntity.getFile();
        try {
            file.transferTo(temporaryFile);
        } catch (IOException e) {
            log.error(e.getMessage());
            e.printStackTrace();
        }
        return "success";
    }

    @RequestMapping(value = "/merge", method = RequestMethod.POST)
    public Object merge(@RequestBody FileMergeReq fileMergeEntity) {
        String finalFilename = fileMergeEntity.getFilename();
        File folder = new File(folderPath);
        //获取暂存切片文件的文件夹中的所有文件
        File[] files = folder.listFiles();
        //合并的文件
        File finalFile = new File(folderPath + "/" + finalFilename);
        String finalFileMainName = finalFilename.split("\\.")[0];
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            outputStream = new FileOutputStream(finalFile, true);
            List<File> list = new ArrayList<>();
            for (File file : files) {
                String filename = FileNameUtil.mainName(file);
                //判断是否是所需要的切片文件
                if (StringUtils.equals(filename, finalFileMainName)) {
                    list.add(file);
                }
            }
            //如果服务器上的切片数量和前端给的数量不匹配
            if (fileMergeEntity.getFileNum() != list.size()) {
                return "文件缺失,请重新上传";
            }
            //根据切片文件的下标进行排序
            List<File> fileListCollect = list.parallelStream().sorted(((file1, file2) -> {
                String filename1 = FileNameUtil.extName(file1);
                String filename2 = FileNameUtil.extName(file2);
                return filename1.compareTo(filename2);
            })).collect(Collectors.toList());
            //根据排序的顺序依次将文件合并到新的文件中
            for (File file : fileListCollect) {
                inputStream = new FileInputStream(file);
                int temp = 0;
                byte[] byt = new byte[2 * 1024 * 1024];
                while ((temp = inputStream.read(byt)) != -1) {
                    outputStream.write(byt, 0, temp);
                }
                outputStream.flush();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if (inputStream != null){
                    inputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (outputStream != null){
                    outputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 产生的文件大小和前端一开始上传的文件不一致
        if (finalFile.length() != Long.parseLong(fileMergeEntity.getFileSize())) {
            return "上传文件大小不一致";
        }
        return "上传成功";
    }
}

为了图方便我就直接return 字符串了 嘿嘿(当然我在这个demo里面写了方法统一结果的封装,所以输出的时候还是restful风格的结果,详细内容可以看我之前的文章《Spring使用AOP完成统一结果封装》)

当前端调用upload接口的时候,后端就会将前端传过来的文件放到一个临时文件夹中

当调用merge接口的时候,后端就会认为分片文件已经全部上传完毕就会进行文件合并的工作

后端主要是根据前端返回的hash值来判断分片文件的顺序

结尾

其实分片上传听起来好像很麻烦,其实只要把思路捋清楚了其实是不难的,是一个比较简单的需求。

当然这个只是一个比较简单一个demo,只是实现的一个较为简单的分片上传功能,像断点上传,上传暂停这些功能暂时还没来得及写到demo里面,之后有时间了会新开一个文章写这些额外的内容。

到此这篇关于Java的分片上传功能的实现的文章就介绍到这了,更多相关Java 分片上传内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯