java实现大文件分片上传功能(前后端都有,代码down下来配置完后可以直接运行)

问题

项目解决的问题主要是java实现分片上传功能,问题描述:
楼主在公司最近项目中使用multipart文件上传视频文件到服务器上,然后用fastdfs保存到数据库中。发现当上传的视频文件太大的时候会使服务器内存的buf/cache占用很高(好几个G),虽然可以手动清除,但是依旧无法从根源上解决视频上传内存占用太大的问题。

在这里插入图片描述


解决问题的思路

lz花了100积分在csdn上面提的问题:急急急,求java上传大文件占用jvm过高的问题解决方案/思路

1、mmf,通过memory mapped file 内存映射文件将数据分段存储到mysql或者其他数据库中,不适合,略
2、服务器上ftp,然后通过代码来借助ftp实现
3、前端通过vue-upload来实现文件的分片上传功能,后端使用fastdfs自带的分片功能实现数据存储(lz的解决方式)



解决的问题/实现的功能

前端实现的功能:simple-uploader.js(也称 Uploader) 是一个上传库,支持多并发上传,文件夹、拖拽、可暂停继续、秒传、分块上传、出错自动重传、手工重传、进度、剩余时间、上传速度等特性;该上传库依赖 HTML5 File API。

后端实现的功能:springboot 整合 fastDfs,redis实现,.本地路径文件分片上传 2.fastDfs 文件上传,下载,分片上传



项目实现技术

先说一下使用到大概的东西,防止大家数据库或者其他原因无法使用,导致浪费时间。
使用到的技术:simple-uploader(前端) + fastdfs(数据库)+ springboot(项目框架)+Redis

项目源地址:

  • 前端: simple-uploader
  • 后端: fastDfs-demo
    注:十分感谢提供开源项目的作者,楼主的代码是根据两个项目改变来的,这两个项目是源码。
  • 楼主自己的代码:
    链接:地址
    提取码:ey36



实现的效果

在这里插入图片描述
在这里插入图片描述



实现原理:

  • 前端使用分片插件后,一个请求会被分成多个请求。多个upload请求均为分片的请求,把大文件分成多个小份一次一次向服务器传递分片完成后,即upload完成后,需要向服务器传递一个merge请求,让服务器将多个分片文件合成一个文件,当我们上传一个大文件时,会被插件分片,ajax请求如下:
    在这里插入图片描述

  • 可以看到发起了多次upload的请求,我们来看看upload发送的具体参数
    在这里插入图片描述

    扫描二维码关注公众号,回复: 11732911 查看本文章
  • 第一个配置(content-disposition)中的guid和第二个配置中的access_token,是我们通过webuploader配置里的formData,即传递给服务器的参数后面几个配置是文件内容,chunkNumber、chunkSize、currentChunkSize等其中totalChunks为总分片数,chunkSize为当前第几个分片。图片中为13。当你看到chunk是130的upload请求时,代表这是最后一个upload请求了

  • 分片后,文件还未整合,数据大概是下面这个样子:
    在这里插入图片描述

  • 后台的验证
    1 在“加入文件”的回调中,通过FileReader读取文件,生成MD5,发给后台
    2.1 如果后台直接返回了“跳过上传”字段和文件的url,则跳过上传,这是秒传;
    2.2 如果后台返回了分片信息,这是断点续传。后台会在每个分片中标识这个分片是否上传过,你需要在分片上传校验的回调中判断,如果true则跳过该分片。
    3 每个分片上传成功,后台都会返回一个字段判断是否需要合并;在“上传完成”的回调中,如果这个字段为true,则需要给后台发一个请求合并的ajax请求

代码分享

楼主前面分享的项目改后的代码

后端核心代码:

api层:

@PostMapping(value = "/fastDfsChunkUpload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Map chunkUpload1(MultipartFileParam multipartFileParam, HttpServletResponse response) {
    
    
        Map<String, String> map = new HashMap<>();
        long chunk = multipartFileParam.getChunkNumber();
        long totalChunk = multipartFileParam.getTotalChunks();
        long chunkSize = multipartFileParam.getChunkSize();
        long historyUpload = (chunk - 1) * chunkSize;
        String md5 = multipartFileParam.getIdentifier();
        MultipartFile file = multipartFileParam.getFile();
        String fileName = FileUtil.extName(file.getOriginalFilename());
        StorePath path = null;
        String groundPath;

        try {
    
    
            if (chunk == 1) {
    
    
                path = appendFileStorageClient.uploadAppenderFile(UpLoadConstant.DEFAULT_GROUP, file.getInputStream(),
                        file.getSize(), fileName);
                if (path == null) {
    
    
                    map.put("result", "上传第一个就错了");
                    response.setStatus(500);
                    return map;
                } else {
    
    
                    redisUtil.setObject(UpLoadConstant.uploadChunkNum + md5, 1, cacheTime);
                    map.put("result", "上传成功");
                }
                groundPath = path.getPath();
                redisUtil.setObject(UpLoadConstant.fastDfsPath + md5, groundPath, cacheTime);

            } else {
    
    
                groundPath = (String) redisUtil.getObject(UpLoadConstant.fastDfsPath + md5);
                appendFileStorageClient.modifyFile(UpLoadConstant.DEFAULT_GROUP, groundPath, file.getInputStream(),
                        file.getSize(), historyUpload);
                Integer chunkNum = (Integer) redisUtil.getObject(UpLoadConstant.uploadChunkNum + md5);
                chunkNum = chunkNum + 1;
                redisUtil.setObject(UpLoadConstant.uploadChunkNum + md5, chunkNum, cacheTime);
            }
            Integer num = (Integer) redisUtil.getObject(UpLoadConstant.uploadChunkNum + md5);
            if (totalChunk == num) {
    
    
                response.setStatus(200);
                map.put("result", "上传成功");
                map.put("path", groundPath);
                redisUtil.del(UpLoadConstant.uploadChunkNum + md5);
                redisUtil.del(UpLoadConstant.fastDfsPath + md5);
            }
        } catch (FdfsIOException | SocketTimeoutException e) {
    
    
            response.setStatus(407);
            map.put("result", "重新发送");
            return map;
        } catch (Exception e) {
    
    
            e.printStackTrace();
            redisUtil.del(UpLoadConstant.uploadChunkNum + md5);
            redisUtil.del(UpLoadConstant.fastDfsPath + md5);
            response.setStatus(500);
            map.put("result", "upload error");
            return map;
        }
        System.out.println("result=" + map.get("result"));
        System.out.println("path=" + map.get("path"));
        return map;
    }

实体类:MultipartFileParam

package com.dgut.fastdfs.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.web.multipart.MultipartFile;

import java.io.Serializable;

/**
 * @author :陈文浩
 * @date :Created in 2019/12/15 12:27
 * @description:
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class MultipartFileParam implements Serializable {
    
    

    private String taskId;//文件传输任务ID
    private long chunkNumber;//当前为第几分片
    private long chunkSize;//每个分块的大小
    private long totalChunks;//分片总数
    private String identifier;//文件唯一标识
    private MultipartFile file;//分块文件传输对象

}

工具类:RedisUtil

package com.dgut.fastdfs.utils;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;


import java.util.List;
import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {
    
    

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;


    //写入对象
    public boolean setObject(final String key, Object value, Integer expireTime) {
    
    
        try {
    
    

            redisTemplate.opsForValue().set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            return true;
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return false;
        }
    }

    //获取对象
    public Object getObject(final String key) {
    
    
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }


    //写入集合
    public boolean setList(final String key, Object value, Integer expireTime) {
    
    
        try {
    
    
            redisTemplate.opsForList().rightPush(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            return true;
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return false;
        }
    }

    //获取集合
    public List<Object> getList(final String key) {
    
    
        try {
    
    
            return redisTemplate.opsForList().range(key, 0, -1);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return null;
        }
    }

    //判断时候存在key
    public boolean hasKey(final String key) {
    
    
        try {
    
    
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        return false;
    }

    //删除key
    public void del(final String key) {
    
    
        if (hasKey(key)) {
    
    
            redisTemplate.delete(key);
        }
    }


}

前端主要需要修改的地方

  • 访问的api
  • 同时上传的分片数量
    在这里插入图片描述
    项目代码已经上传到百度网盘上面去了,可以自己去下载

尾声

lz花了两个小时的时间整理资源,忘大家看到后别忘了点个赞,谢谢


来生还长,切勿惆怅

猜你喜欢

转载自blog.csdn.net/qq_42910468/article/details/108607427
今日推荐