java|springboot+webuploader 实现大文件切片上传,兼容IE8+,chrome等浏览器,附可运行代码

一、目标
主要解决在web端浏览器上传大文件问题,避免上传途中中断、卡死、jvm溢出等问题,支持进度条显示、秒传功能、多线程上传,兼容IE8+、chrome等主流浏览器
二、方案
前端借助百度提供的webuploader插件,对大文件计算MD5值并切成一个个小文件,多线程向后台上传;后台用springboot实现,根据MD5和接收的个数判断是否接收完毕,如果接收完毕进行后台合并操作。
三、前端部分
Html代码

选择文件 上传

【java|springboot+webuploader 实现大文件切片上传,兼容IE8+,chrome等浏览器,附可运行代码】Js代码
var fileMd5; var state; var uploader; var chunkSize=5 * 1024 * 1024; var access_token="0010"; var user_id="1"; $(function() { //监听分块上传过程中的三个时间点 WebUploader.Uploader.register({ "before-send-file" : "beforeSendFile", "before-send" : "beforeSend", "after-send-file" : "afterSendFile", }, { //时间点1:所有分块进行上传之前调用此函数 beforeSendFile : function(file) { var deferred = WebUploader.Deferred(); var owner = this.owner; //1、计算文件的唯一标记,用于断点续传 (new WebUploader.Uploader()).md5File(file, 0, chunkSize) .progress(function(percentage) { $('#upload_info').find("p.state").text("正在读取文件信息..."); }).then(function(val) { fileMd5 = val; $('#upload_info').find("p.state").text("成功获取文件信息..."); //2、判断文件是否已存在 $.ajax({ type : "GET", url : webUploadAddress + "/resource/findResourceFileExistsByMd5", data : { access_token:access_token, user_id:user_id, //文件唯一标记 md5value : fileMd5 }, dataType : "json", async:false, success : function(response) { if (response.code==2000) { if (response.data.isExist) { //文件已上传,跳过 console.log("文件已上传"); deferred.reject(); owner.skipFile(file); } else { //获取文件信息后进入下一步 deferred.resolve(); } } } }); }); return deferred.promise(); }, //时间点2:如果有分块上传,则每个分块上传之前调用此函数 beforeSend : function(block) { var deferred = WebUploader.Deferred(); $.ajax({ type : "GET", url : webUploadAddress + "/resource/findResourceChunkExists", data : { access_token:access_token, user_id:user_id, //文件唯一标记 md5value : fileMd5, //当前分块下标 chunk : block.chunk, //当前分块大小 chunkSize : block.end - block.start }, dataType : "json", async:false, success : function(response) { if (response.code==2000) { if (response.data.isExist) { //分块存在,跳过 console.log("分块存在,跳过"); deferred.reject(); } else { //分块不存在或不完整,重新发送该分块内容 console.log("分块不存在或不完整,重新发送该分块内容"); deferred.resolve(); } } } }); this.owner.options.formData.md5value =https://www.it610.com/article/fileMd5; this.owner.options.formData.chunk =block.chunk; this.owner.options.formData.access_token =access_token; this.owner.options.formData.user_id =user_id; deferred.resolve(); return deferred.promise(); }, //时间点3:所有分块上传成功后调用此函数 afterSendFile : function() { //如果分块上传成功,则获取上传结果 $.ajax({ type :"GET", url : webUploadAddress + "/resource/findUploadResult", data : { access_token:access_token, user_id:user_id, //文件唯一标记 md5value : fileMd5 }, dataType : "json", async:false, success : function(response) { if (response.code==2000) { if (response.data) { console.log("如果分块上传成功,则获取上传结果",response.data); } } } }); } }); uploader = WebUploader .create({ method:'POST', // swf文件路径 swf :Global.assets + '/resource/thirdparty/webuploader-0.1.5/Uploader.swf', // 文件接收服务端。 server : webUploadAddress + '/resource/upload', // 选择文件的按钮。可选。 // 内部根据当前运行是创建,可能是input元素,也可能是flash. pick :'#picker', resize : false,// 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传! auto : false, prepareNextFile :true,//是否允许在文件传输时提前把下一个文件准备好 chunked : true,//开启分片上传 chunkRetry:3,//如果某个分片由于网络问题出错,允许自动重传多少次? chunkSize :chunkSize, duplicate :true, formData:{ title:'视频上传', resourceType:'video/mp4', guid:"SD.mp4" }, threads:3, accept : { //限制上传文件为MP4 //extensions : 'mp4', //mimeTypes : 'video/mp4', } }); //当有文件被添加进队列的时候 uploader.on('beforeFileQueued', function(file) { //清空队列 uploader.reset(); }); //当有文件被添加进队列的时候 uploader.on('fileQueued', function(file) { $('#upload_info').empty(); $('#upload_info').html( '' + '' + file.name + '
' + '等待上传...
'); }); //文件上传过程中创建进度条实时显示。 uploader.on('uploadProgress', function(file, percentage) { //进度最大99%,不然后台合并时会在100%卡一会,造成用户疑惑 var jd=Math.round(percentage * 100); if(jd>99){ jd=99; } $('#upload_info').find('p.state').text( '上传中 ' + jd + '%'); }); uploader.on('uploadSuccess', function(file) { $('#upload_info').find('p.state').text( '上传中 ' + 100 + '%'); $('#' + file.id).find('p.state').text('上传成功!'); }); uploader.on('uploadError', function(file) { $('#' + file.id).find('p.state').text('上传出错!'); }); uploader.on('uploadComplete', function(file) { $('#' + file.id).find('.progress').fadeOut(); resetUpload(); }); uploader.on( 'all', function( type ) { if ( type === 'startUpload' ) { state = 'uploading'; } else if ( type === 'stopUpload' ) { state = 'paused'; } else if ( type === 'uploadFinished' ) { state = 'done'; } }); }); function resetUpload() { uploader.upload(); $('#btn').attr("onclick", "stopUpload()"); $('#btn').text("上传"); } function startUpload() { uploader.upload(); $('#btn').attr("onclick", "stopUpload()"); $('#btn').text("取消上传"); } function stopUpload() { uploader.stop(true); $('#btn').attr("onclick", "startUpload()"); $('#btn').text("继续上传"); }

四、后端部分
接收代码
package com.demo.fileupload.controller; import java.util.Collections; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import com.demo.fileupload.dto.ResponseEx; import com.demo.fileupload.entity.Resource; import com.demo.fileupload.entity.ResourceFile; import com.demo.fileupload.service.ResourceService; import com.demo.fileupload.vo.FileSaveInfo; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; /** * 资源上传Controller * * @authorzjx * @date2018年10月29日 上午10:44:22 * @version V1.0 * @history 2018年10月29日 上午10:44:22 create * @Description * */ @Api(tags="资源", description = "资源相关api文档接口列表") @RestController @RequestMapping("/resource") public class ResourceController extends BaseUploadController {@Autowired private ResourceService resourceService; @ApiOperation(value="https://www.it610.com/article/资源上传", notes="资源上传接口,swagger不能模拟分片功能,需百度的webuploader或者plupload等支持分片功能的上传组件支持",httpMethod = "POST", produces = MediaType.APPLICATION_JSON_VALUE) @ApiImplicitParams({ @ApiImplicitParam(name = "access_token", value = "https://www.it610.com/article/鉴权token", required = true, dataType = "String", paramType="query"), @ApiImplicitParam(name = "user_id", value = "https://www.it610.com/article/用户ID", required = true, dataType = "String", paramType="query"), @ApiImplicitParam(name = "title", value = "https://www.it610.com/article/资源标题", required = true, dataType = "String", paramType="query"), @ApiImplicitParam(name = "resourceType", value = "https://www.it610.com/article/资源类型", required = true, dataType = "String", paramType="query"), @ApiImplicitParam(name = "brief", value = "https://www.it610.com/article/资源备注说明", required = false, dataType = "String", paramType="query"), @ApiImplicitParam(name = "guid", value = "https://www.it610.com/article/临时文件名", required = false, dataType = "String", paramType="query"), @ApiImplicitParam(name = "md5value", value = "https://www.it610.com/article/客户端生成md5值", required = false, dataType = "String", paramType="query"), @ApiImplicitParam(name = "chunks", value = "https://www.it610.com/article/总分片数", required = false, dataType = "String", paramType="query"), @ApiImplicitParam(name = "chunk", value = "https://www.it610.com/article/当前分片序号", required = false, dataType = "String", paramType="query"), @ApiImplicitParam(name = "name", value = "https://www.it610.com/article/上传文件名", required = false, dataType = "String", paramType="query") }) @PostMapping("/upload") public ResponseEx upload( String access_token, String user_id, String title, String resourceType, String brief, String guid, String md5value, String chunks, String chunk, String name, @RequestParam(value = "https://www.it610.com/article/file",required = false) MultipartFile file){ Assert.notNull(title,"资源文件标题不能为空"); Assert.notNull(resourceType,"资源文件类型不能为空"); Resource resource = new Resource(); resource.setBrief(brief); resource.setName(title); resource.setType(resourceType); resource.setUserId(user_id); if(file == null){ Assert.notNull(md5value,"MD5值不能为空"); ResourceFile resourceFile = resourceService.findFirstByMd5Value(md5value); Assert.notNull(resourceFile,"资源文件不存在!"); resource.setFileId(resourceFile.getId()); resourceService.saveUpload(resource); }else { //文件保存 FileSaveInfo info = fileUpload(guid, md5value, chunks, chunk, name, file); if (info != null) { info.setType("resource"); resourceService.saveUpload(resource, info); } } return new ResponseEx(); }@ApiOperation(value="https://www.it610.com/article/根据md5值判断资源文件是否存在", notes="根据md5值判断资源文件是否存在",httpMethod = "GET", produces = MediaType.APPLICATION_JSON_VALUE) @ApiImplicitParams({ @ApiImplicitParam(name = "access_token", value = "https://www.it610.com/article/鉴权token", required = false, dataType = "String", paramType="query"), @ApiImplicitParam(name = "user_id", value = "https://www.it610.com/article/用户ID", required = true, dataType = "String", paramType="query"), @ApiImplicitParam(name = "md5value", value = "https://www.it610.com/article/文件md5值", required = true, dataType = "String", paramType="query") }) @GetMapping("/findResourceFileExistsByMd5") public ResponseEx findResourceFileExists( String access_token, String user_id,String md5value){ ResourceFile resourceFile = resourceService.findFirstByMd5Value(md5value); return new ResponseEx().data(Collections.singletonMap("isExist",resourceFile != null)); }@ApiOperation(value="https://www.it610.com/article/根据md5值和分片下标判断当前分片是否存在", notes="根据md5值和分片下标判断当前分片是否存在",httpMethod = "GET", produces = MediaType.APPLICATION_JSON_VALUE) @ApiImplicitParams({ @ApiImplicitParam(name = "access_token", value = "https://www.it610.com/article/鉴权token", required = false, dataType = "String", paramType="query"), @ApiImplicitParam(name = "user_id", value = "https://www.it610.com/article/用户ID", required = true, dataType = "String", paramType="query"), @ApiImplicitParam(name = "md5value", value = "https://www.it610.com/article/文件md5值", required = true, dataType = "String", paramType="query"), @ApiImplicitParam(name = "chunk", value = "https://www.it610.com/article/分片下标", required = true, dataType = "Integer", paramType="query"), @ApiImplicitParam(name = "chunkSize", value = "https://www.it610.com/article/分片大小", required = true, dataType = "Integer", paramType="query") }) @GetMapping("/findResourceChunkExists") public ResponseEx findResourceChunkExists( String access_token, String user_id,String md5value,Integer chunk,Integer chunkSize){ boolean isExist=resourceService.findResourceChunkExists(md5value,chunk,chunkSize); return new ResponseEx().data(Collections.singletonMap("isExist",isExist)); }@ApiOperation(value="https://www.it610.com/article/根据md5值获取上传结果", notes="根据md5值获取上传结果",httpMethod = "GET", produces = MediaType.APPLICATION_JSON_VALUE) @ApiImplicitParams({ @ApiImplicitParam(name = "access_token", value = "https://www.it610.com/article/鉴权token", required = false, dataType = "String", paramType="query"), @ApiImplicitParam(name = "user_id", value = "https://www.it610.com/article/用户ID", required = true, dataType = "String", paramType="query"), @ApiImplicitParam(name = "md5value", value = "https://www.it610.com/article/文件md5值", required = true, dataType = "String", paramType="query") }) @GetMapping("/findUploadResult") public ResponseEx findUploadResult( String access_token, String user_id,String md5value){ FileSaveInfo fileSaveInfo=resourceService.findUploadResult(md5value); return new ResponseEx().data(fileSaveInfo); }}

合并主要代码
/** * 上传文件 * @param md5MD5 * @param guid随机生成的文件名 * @param chunk文件分块序号 * @param chunks文件分块数 * @param fileName文件名 * @param ext文件后缀名 * @return FileSaveInfo文件保存信息对象 如果全部分片保存成功则返回对象,还没成功返回空 */ public static FileSaveInfo uploaded( final String md5, String guid, final String chunk, final String chunks, final String uploadFolderPath, final String fileName, final String name, final String ext) throws Exception { synchronized (uploadInfoList) { uploadInfoList.add(new UploadInfo(md5, chunks, chunk, uploadFolderPath, fileName, ext,new Date())); } boolean allUploaded = isAllUploaded(md5, chunks); int chunksNumber = Integer.parseInt(chunks); if (allUploaded) { //判断是否自动命名 if(FileUtil.getPropertiesValue("file.save.name.auto").equals("true")){ guid = UUID.randomUUID().toString(); } String lastSavePath = mergeFile(chunksNumber, ext, guid, uploadFolderPath,md5); File file = new File(lastSavePath); FileSaveInfo info = new FileSaveInfo(); info.setSaveName(guid+ext); info.setMd5(md5); info.setSize(file.length()); info.setPath(lastSavePath); info.setFix(ext); info.setRelativePath(relativePath(FileUtil.getSavePath(),lastSavePath)); info.setName(name); info.setCreateDate(new Date()); //合并成功的临时存储 synchronized (mergeSuccessList) { mergeSuccessList.add(info); } return info; }else{ return null; } }

--------------------------------------->代码下载<---------------------------------------

    推荐阅读