博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
SpringBoot+Vue.js前后端分离实现大文件分块上传
阅读量:7087 次
发布时间:2019-06-28

本文共 8641 字,大约阅读时间需要 28 分钟。

原文地址:

博客地址:
欢迎转载,转载请注明作者及出处,谢谢!

SpringBoot+Vue.js前后端分离实现大文件分块上传

之前写过一篇前后端分离实现文件上传的博客,但是那篇博客主要针对的是小文件的上传,如果是大文件,一次性上传,将会出现不可预期的错误。所以需要对大文件进行分块,再依次上传,这样处理对于服务器容错更好处理,更容易实现断点续传、跨浏览器上传等功能。本文也会实现断点,跨浏览器继续上传的功能。

开始

GIF效果预览

此处用到了 的Vue上传组件,此图也是引用自他的GitHub,感谢这位大佬。

需要准备好基础环境

  • Java
  • Node
  • MySQL

准备好这些之后,就可以往下看了。

后端

新建一个SpringBoot项目,我这里使用的是SpringBoot2,引入mvc,jpa,mysql相关的依赖。

org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-test
test
mysql
mysql-connector-java
org.projectlombok
lombok
${lombok.version}
复制代码

在yml中配置mvc以及数据库连接等属性

server:  port: 8081  servlet:    path: /bootspring:  servlet:    multipart:      max-file-size: 20MB      max-request-size: 20MB  datasource:    url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&useSSL=false    username: root    password: root    driver-class-name: com.mysql.jdbc.Driver  jpa:    properties:      hibernate:        hbm2ddl:          auto: create-drop    show-sql: truelogging:  level:    org.boot.uploader.*: debugprop:  upload-folder: files复制代码

定义文件上传相关的类,一个是FileInfo,代表文件的基础信息;一个是Chunk,代表文件块。

FileInfo.java

@Data@Entitypublic class FileInfo implements Serializable {    @Id    @GeneratedValue    private Long id;    @Column(nullable = false)    private String filename;    @Column(nullable = false)    private String identifier;    @Column(nullable = false)    private Long totalSize;    @Column(nullable = false)    private String type;    @Column(nullable = false)    private String location;}复制代码

Chunk.java

@Data@Entitypublic class Chunk implements Serializable {    @Id    @GeneratedValue    private Long id;    /**     * 当前文件块,从1开始     */    @Column(nullable = false)    private Integer chunkNumber;    /**     * 分块大小     */    @Column(nullable = false)    private Long chunkSize;    /**     * 当前分块大小     */    @Column(nullable = false)    private Long currentChunkSize;    /**     * 总大小     */    @Column(nullable = false)    private Long totalSize;    /**     * 文件标识     */    @Column(nullable = false)    private String identifier;    /**     * 文件名     */    @Column(nullable = false)    private String filename;    /**     * 相对路径     */    @Column(nullable = false)    private String relativePath;    /**     * 总块数     */    @Column(nullable = false)    private Integer totalChunks;    /**     * 文件类型     */    @Column    private String type;    @Transient    private MultipartFile file;}复制代码

编写文件块相关的业务操作

@Servicepublic class ChunkServiceImpl implements ChunkService {    @Resource    private ChunkRepository chunkRepository;    @Override    public void saveChunk(Chunk chunk) {        chunkRepository.save(chunk);    }    @Override    public boolean checkChunk(String identifier, Integer chunkNumber) {        Specification
specification = (Specification
) (root, criteriaQuery, criteriaBuilder) -> { List
predicates = new ArrayList<>(); predicates.add(criteriaBuilder.equal(root.get("identifier"), identifier)); predicates.add(criteriaBuilder.equal(root.get("chunkNumber"), chunkNumber)); return criteriaQuery.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction(); }; return chunkRepository.findOne(specification).orElse(null) == null; }}复制代码
  1. checkChunk()方法会根据文件唯一标识,和当前块数判断是否已经上传过这个块。
  2. 这里只贴了ChunkService的代码,其他的代码只是jpa简单的存取。

接下来就是编写最重要的controller了

@RestController@RequestMapping("/uploader")@Slf4jpublic class UploadController {    @Value("${prop.upload-folder}")    private String uploadFolder;    @Resource    private FileInfoService fileInfoService;    @Resource    private ChunkService chunkService;    @PostMapping("/chunk")    public String uploadChunk(Chunk chunk) {        MultipartFile file = chunk.getFile();        log.debug("file originName: {}, chunkNumber: {}", file.getOriginalFilename(), chunk.getChunkNumber());        try {            byte[] bytes = file.getBytes();            Path path = Paths.get(generatePath(uploadFolder, chunk));            //文件写入指定路径            Files.write(path, bytes);            log.debug("文件 {} 写入成功, uuid:{}", chunk.getFilename(), chunk.getIdentifier());            chunkService.saveChunk(chunk);            return "文件上传成功";        } catch (IOException e) {            e.printStackTrace();            return "后端异常...";        }    }    @GetMapping("/chunk")    public Object checkChunk(Chunk chunk, HttpServletResponse response) {        if (chunkService.checkChunk(chunk.getIdentifier(), chunk.getChunkNumber())) {            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);        }        return chunk;    }    @PostMapping("/mergeFile")    public String mergeFile(FileInfo fileInfo) {        String path = uploadFolder + "/" + fileInfo.getIdentifier() + "/" + fileInfo.getFilename();        String folder = uploadFolder + "/" + fileInfo.getIdentifier();        merge(path, folder);        fileInfo.setLocation(path);        fileInfoService.addFileInfo(fileInfo);        return "合并成功";    }}复制代码
  1. 文章开头就提到了前后端分离,既然是前后端分离,肯定会涉及到跨域问题,在上一篇文章中是通过springMVC的@CrossOrigin注解来解决跨域问题,这里并没有使用这个注解,在下面的前端项目中会使用一个node的中间件来做代理,解决跨域的问题。
  2. 可以看到有两个/chunk路由,第一个是post方法,用于上传并存储文件块,需要对文件块名进行编号,再存储在指定路径下;第二个是get方法,前端上传之前会先进行检测,如果此文件块已经上传过,就可以实现断点和快传。
  3. /mergeFile用于合并文件,在所有块上传完毕后,前端会调用此接口进行制定文件的合并。其中的merge方法是会遍历指定路径下的文件块,并且按照文件名中的数字进行排序后,再合并成一个文件,否则合并后的文件会无法使用,代码如下:
public static void merge(String targetFile, String folder) {        try {            Files.createFile(Paths.get(targetFile));            Files.list(Paths.get(folder))                    .filter(path -> path.getFileName().toString().contains("-"))                    .sorted((o1, o2) -> {                        String p1 = o1.getFileName().toString();                        String p2 = o2.getFileName().toString();                        int i1 = p1.lastIndexOf("-");                        int i2 = p2.lastIndexOf("-");                        return Integer.valueOf(p2.substring(i2)).compareTo(Integer.valueOf(p1.substring(i1)));                    })                    .forEach(path -> {                        try {                            //以追加的形式写入文件                            Files.write(Paths.get(targetFile), Files.readAllBytes(path), StandardOpenOption.APPEND);                            //合并后删除该块                            Files.delete(path);                        } catch (IOException e) {                            e.printStackTrace();                        }                    });        } catch (IOException e) {            e.printStackTrace();        }    }复制代码

到这里,后端主要的逻辑已经写完了,下面开始编写前端的部分。

前端

前端我直接clone了,在这个代码的基础上进行了修改。

App.vue

...复制代码

配置说明:

  1. target 目标上传 URL,可以是字符串也可以是函数,如果是函数的话,则会传入 Uploader.File 实例、当前块 Uploader.Chunk 以及是否是测试模式,默认值为 '/'。
  2. chunkSize 分块时按照该值来分。最后一个上传块的大小是可能是大于等于1倍的这个值但是小于两倍的这个值大小,默认 110241024。
  3. testChunks 是否测试每个块是否在服务端已经上传了,主要用来实现秒传、跨浏览器上传等,默认true。
  4. simultaneousUploads 并发上传数,默认3。

更多说明请直接参考

解决跨域问题

这里使用了http-proxy-middleware这个node中间件,可以对前端的请求进行转发,转发到指定的路由。

在index.js中进行配置,如下:

dev: {    env: require('./dev.env'),    port: 8080,    autoOpenBrowser: true,    assetsSubDirectory: '',    assetsPublicPath: '/',    proxyTable: {      '/boot': {        target: 'http://localhost:8081',        changeOrigin: true  //如果跨域,则需要配置此项      }    },    // CSS Sourcemaps off by default because relative paths are "buggy"    // with this option, according to the CSS-Loader README    // (https://github.com/webpack/css-loader#sourcemaps)    // In our experience, they generally work as expected,    // just be aware of this issue when enabling this option.    cssSourceMap: false  }复制代码

proxyTable表示代理配置表,将特定的请求代理到指定的API接口,这里是将'localhost:8080/boot/xxx'代理到'http://localhost:8081/boot/xxx'。

现在可以开始验证了,分别启动前后端的项目

  • 前端
npm installnpm run dev复制代码
  • 后端 可以通过command line,也可以直接运行BootUploaderApplication的main()方法

运行效果就像最开始的那张图,可以同时上传多个文件,上传暂停之后更换浏览器,选择同一个文件可以实现继续上传的效果,大家可以自行进行尝试,代码会在我的上进行更新。

最后

整篇文章到这里差不多就结束了,这个项目可以作为demo用来学习,有很多可以扩展的地方,肯定也会有不完善的地方,有更好的方法也希望能指出,共同交流学习。

你可能感兴趣的文章
JSR与MR的区别
查看>>
华为存储不是昙花一现
查看>>
沟通是一种感知
查看>>
学会这二十个正则表达式,能让你少些1000行代码!
查看>>
关于Cocos Creator脚本执行顺序的几点补充
查看>>
Powershell-Exchange:设置分层通讯薄中通讯组的优先级
查看>>
开启好用的Lync联系人即时模糊搜索功能
查看>>
Microsoft Hyper-V Server 2012开启虚拟化-SMB 3.0
查看>>
Powershell管理系列(十二)Exchange新启用的邮箱禁用OWA及Activesync的访问
查看>>
Windows 8上安装本地回环网卡
查看>>
Exchange Server 2013系列十二:邮箱的基本管理
查看>>
[C#进阶系列]专题二:你知道Dictionary查找速度为什么快吗?
查看>>
并发连接数、请求数、并发用户数
查看>>
SDA报告给各国网络空间安全防卫水平进行评级
查看>>
去小机化思维(二)--【软件和信息服务】2015.03
查看>>
【翻译】Sencha Cmd中脚本压缩方法之比较
查看>>
最新.NET 5.0 C#6 MVC6 WCF5 NoSQL Azure开发120课视频
查看>>
爱因斯坦计划最新进展(201710)
查看>>
传统HA系统的终结者-【软件和信息服务】2013.11
查看>>
Spread for Windows Forms快速入门(15)---使用 Spread 设计器
查看>>