根据项目需要,在很多地方都需要将超大文件上传到服务器,特别是将视频文件上传到云平台的OSS服务器上,这种需求在项目中已经是十分普遍的需求了。在网上收集了很多资料,基本上都只有JAVA到OSS服务器,或都VUE前端服务端到Spring Boot后端服务器。当前前后端开发已经十分普及了,大文件一般都保存到OSS服务器,不会保存到自己的业务服务器,OSS大文件服务器+tomcat为业务服务器+VUE/Ract/小程序前端服务器的三层后台架构是当前最主流的架构设计,因此必须解决这个问题。
通过几天的反复研究和实践,结合网上找到的资料,需要将VUE服务端到Spring Boot服务器和JAVA到OSS服务器两部分结合起来,形成整体的解决方案。
在这个整体中有两上难点:
1、VUE前端服务器的JS程序是异步的,而大文件分片传输需要大量分次请求,请求执行完所顺序是不一致的;
2、由于是三层请求,涉及到多用户,Spring Boot服务器需要将分原来整体功能,撤分为三个,存在中间缓存信息持久化保存与多用户隔离问题
一、解决方案框架
由于这个功能还是非常复杂,为了解决这个问题,还是绘制了功能框架图,并且在具体编程时,对功能框架图进行了多次修正,最终框架如下图所示。
1、 框架整体上是前端判断是否大文件,超过10M,如果向Spring Boot服务器,请求大文件传输,Spring Boot向OSS服务器申请创新大文件传递的uploadID.
2、前端对文件进行分片,并产生分片MD5码,根据第1得到的id发起分片传输,Spring Boot收到分片后,上传分片
3、Spring boot在redis中缓存存文件ID,分片信息,以备文件合成用
4、前端检索返回MD5,如果有错,重新传递分片
5、前端检查分片是否全部正确传输完成,如果传递完成,发起文件合并请求
6、Spring boot将缓存信息组合,向OSS提出文件合并请求
7、OSS服务器收到相关信息,检验后合成文件并返回文件访问地址
8、Spring boot将访问地址及相关信息保存到服务器,并清除缓存
9、前端收到信息,显示进展,请除缓存存
10、根据地址使用已经上传的文件,如播放或预览。
二、Spring Boot后台程序
后台程序分为Controller和utils两部分,controller与前端交互,utils与OSS交互。
2.1、前端与用户交互的Controller
由于前后端分离,需要将大文件上传分为三个部分,即大文件上传初始化、上传大文件块、合并已上传文件,分别与VUE前端进行交互。
/**
* <p>
* 前端控制器
* </p>
*
* @author wu jize
* @since 2020-07-05
*/
@RestController
@RequestMapping("/map/scenic/file")
public class MapBigFileController extends BaseController {
@Autowired
private IMapScenicFileService iMapScenicFileService;
@Autowired
IMapScenicService iMapScenicService;
@Autowired
private TokenService tokenService;
@Autowired
private BigFileUploadUtils bigFileUploadUtils;
/**
* 大文件上传初始化
* 返回文件uploadId
*/
@ApiOperation("大文件上传初始化")
@PreAuthorize("@ss.hasPermi('data:scenicfile:edit')")
@Log(title = "大文件上传初始化", businessType = BusinessType.UPDATE)
@PostMapping("/initUpload")
public AjaxResult initBigFileUpload(@RequestParam(name = "fileName", required = true) String fileName,
@RequestParam(name = "scenicId", required = false) Long scenicId) {
//格式化文件路径,按县、景区ID、用户名组织文件
String adcode = iMapScenicService.selectScenicById(scenicId).getAdcode();
if (StringUtils.isEmpty(adcode)) {
throw new CustomException("景区行政区域编码为空,不能添加景区多媒体文件!");
}
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
String author = loginUser.getUsername();
String objectName = "scenicfile" + "/" + adcode + "/" + scenicId.toString() + "/" + author + '/' + fileName;
String uploadId = bigFileUploadUtils.initUpload(objectName);
AjaxResult ajax = AjaxResult.success();
ajax.put("uploadId", uploadId);
return ajax;
}
/**
* 上传大文件的Chunk
* 返回chunk的MD5
*/
@ApiOperation("上传大文件的Chunk")
@PreAuthorize("@ss.hasPermi('data:scenicfile:edit')")
@Log(title = "上传大文件的Chunk", businessType = BusinessType.UPDATE)
@PostMapping("/uploadChunk")
public AjaxResult uploadChunk(@RequestParam("chunkFile") MultipartFile chunkFile,
@RequestParam(name = "uploadId", required = true) String uploadId,
@RequestParam(name = "chunkId", required = true) Integer chunkId,
@RequestParam(name = "total", required = true) Integer total) throws IOException {
String md5Str = bigFileUploadUtils.uploadChunk(uploadId, chunkId, chunkFile);
AjaxResult ajax = AjaxResult.success();
ajax.put("md5Str", md5Str);
return ajax;
}
/**
* 大文件上传完成后合并
* 返回文件访问的URL
*/
@ApiOperation("大文件上传完成后合并")
@PreAuthorize("@ss.hasPermi('data:scenicfile:edit')")
@Log(title = "大文件上传完成后合并", businessType = BusinessType.UPDATE)
@PostMapping("/mergeFile")
public AjaxResult mergeFile(@RequestParam(name = "uploadId", required = true) String uploadId) {
AjaxResult ajax = AjaxResult.success();
String url = bigFileUploadUtils.completeFile(uploadId);
ajax.put("url", url);
return ajax;
}
}
2.2、util包,负责与OSS交互。
在后端与OSS交互中,有一个难点是解决大文件前后端分离后,异步数据上传中该文件相关信息的保存。由于是异步请求,方法分为三段执行,不同请求对于Spring Boot来说就是不同的线程,试了以下几种方法,最后用redis缓存存:
1、用包类静态全局变量,由是不同请求会产生不同线程,此方法行不通
2、用线程变量,静态全局变量不行就考虑过用线程变量,功能实现后现还是不行,进一步研究,发现前端不同请求在后端仍然在不同线程,经如初始化大文件上传的uploadId和objectId,在后面请求访问不到。
3、用session变量,研究了一下,发现更加复杂,还不一定能解决得好并好用户,同一用户不同窗口上传问题
4、反馈给前端,uploadId和objectId还行,但是分片的partETag信息太多,处理起来会十分繁琐
5、用数据库临时表进行持久化,也挺复杂,并且还要引入JPA,如用mybatis工作也不少
6、经过多方比较,还是用redis,简单并且速度快,唯一缺点时redis服务器重启后,没有上传完的片断会变成数据垃圾,还没搞清楚阿里OSS怎么处理,不知道要不要占自己的存储容量。Redis根据uploadId保存信息,不同用户,即使同一用户同时上传不同文件其uploadId是唯一的,这样能做好多用户相互隔离问题。
/**
* 用于超过10M的大文件分片上传
*
* @author Wu Jize
* @version 1.0
* @date 2020/7/5 18:28
*/
@Component
public class BigFileUploadUtils {
protected static final Logger log = LoggerFactory.getLogger(OSSFileUtils.class);
//将OSS访问参数保存到配置文件,并忽略上传GIT,避免有权限的数据泄漏
@Value("${aliyun.oss.endpoint}")
private String endpoint;