1.场景描述:
之前用阿里云上传视频,前端反应上传视频经常出现获取视频url失败问题.但是接口我测过很多遍都是没有问题的.后台这边提供了一个视频上传的接口返回一个videoId,还提供了一个根据videoId获取视频url的接口.前端把两个接口的调用都封装在一个方法中,视频上传完成之后直接根据视频id获取视频url.这样会出现一个问题,视频文件如果时间太长,所以前端页面会请求超时,所以会上传失败.商量了一下就是将接口分开来,页面上除了有一个上传的接口,单独提供一个点击查看上传视频的按钮,分别调用两个接口.但是这样问题又来了,如何保证用户上传之后再点击查看按钮调用根据视频id获取视频url的接口,解决方案就是添加一个进度条,显示一下阿里云上传文件的进度.
2.添加上传进度过程中遇到的问题以及解决方案
①阿里云官方的api中有进度回调方法,但是只能是从服务端获取到,如何实时传递到浏览器端.自己想到的方案:第一种是使用redis存储这种随时变化的数据(由于项目中没有使用redis,所以鉴于搭建redis环境考虑就果断放弃此方案);第二种是使用数据库存储(由于对数据库添加以及删除操作过于频繁所有放弃此方案);第三方就是数据存储于服务器端的session解决方案更适合本场景.这也是网上使用很多的方案.具体实现逻辑:除了原有两个接口之外需要添加一个获取上传进度的接口,页面添加一个定时器需要定时的请求此接口刷新进度条,实现上传进度显示.
②技术方案定好之后遇到的问题:每次调用获取进度的接口返回的进度都是0,但是实际上视频都是正常上传的,上传的进度都能通过日志进行打印.阿里云进度监听的类中确实把进度存入session中了.原因:调试发现请求进度接口中的session与上传文件的请求中根据request获取的session不是一个session对象.这就导致每次从session中获取都是0,这里需要说一下session的获取问题.
2.1首先说一下使用request获取session的方式以及区别:
request.getSession(true):若存在会话则返回该会话,否则新建一个会话(request.getSession()默认就是true,通过查看源码可见)。
request.getSession(false):若存在会话则返回该会话,否则返回NULL;
2.2获取session原理:浏览器与服务端交互时,第一次发送请求时会创建session对象,返回JssionId信息并缓存在浏览器中cookie中,下次发送请求会携带该jssionId,使用request获取session对象时会根据是否有jsessionId判断直接获取session对象还是直接创建.
2.3至于两个请求中request如何保证session对象唯一在于:上传文件接口中虽然使用request获取session并往session中添加进度信息,但是此时调用获取进度接口中根据request获取session对象由于不存在JssionId(因为上传接口还未结束,不会响应JssionId信息),所以会重新创建一个session对象.
2.4此问题的解决方案就是保证两个接口通过request获取的session一致,我想到的解决方案就是,先调用获取进度接口,此时返回的进度是0,但是浏览器也缓存了jessionId信息,然后调用文件上传接口,请求中会携带该jessionId,此时文件上传接口中使用request获取的session一定与获取进度接口中的session对象一致.当然要注意在文件上传成功之后请求session信息,否则下次请求进度接口显示的进度是从百分之百开始的了.此处使用的是session自杀方式:session.invalidate();
3.部分主要代码:
文件上传逻辑:
public ResultVo uploadVideo(String title,String fileName,HttpServletRequest httpServletRequest) {
ResultVo resultVo = new ResultVo();
// 判空处理
if(StringUtils.isEmpty(title)||StringUtils.isEmpty(fileName)){
resultVo.setSuccess(false);
resultVo.setMsg("视频上传失败:视频标题或是视频文件路径为空");
return resultVo;
}
// 文件类型校验
// 对上传的视频格式进行校验: 这里简单校验后缀名
boolean isLegal = false;
for (String type : VIDEO_TYPE) {
if (StringUtils.endsWithIgnoreCase(fileName, type)) {
isLegal = true;
break; // 只要与允许上传格式其中一个匹配就可以
}
}
// 格式错误, 返回与前端约定的error
if (!isLegal) {
resultVo.setSuccess(false);
resultVo.setMsg("视频上传失败:"+"视频格式错误,支持格式:.mp4,.rmvb,.rm,.avi,.flv");
return resultVo;
}
UploadVideoRequest request = new UploadVideoRequest(aliyunConfig.getVideoAccessKeyId(), aliyunConfig.getVideoAccessKeySecret(), title, fileName);
/* 可指定分片上传时每个分片的大小,默认为1M字节 */
//request.setPartSize(1 * 1024 * 1024L);
/* 可指定分片上传时的并发线程数,默认为1,(注:该配置会占用服务器CPU资源,需根据服务器情况指定)*/
//request.setTaskNum(1);
/* 是否开启断点续传, 默认断点续传功能关闭。当网络不稳定或者程序崩溃时,再次发起相同上传请求,可以继续未完成的上传任务,适用于超时3000秒仍不能上传完成的大文件。
注意: 断点续传开启后,会在上传过程中将上传位置写入本地磁盘文件,影响文件上传速度,请您根据实际情况选择是否开启*/
//request.setEnableCheckpoint(false);
/* OSS慢请求日志打印超时时间,是指每个分片上传时间超过该阈值时会打印debug日志,如果想屏蔽此日志,请调整该阈值。单位: 毫秒,默认为300000毫秒*/
//request.setSlowRequestsThreshold(300000L);
/* 可指定每个分片慢请求时打印日志的时间阈值,默认为300s*/
//request.setSlowRequestsThreshold(300000L);
/* 是否使用默认水印(可选),指定模板组ID时,根据模板组配置确定是否使用默认水印*/
//request.setIsShowWaterMark(true);
/* 自定义消息回调设置(可选),参数说明参考文档 https://help.aliyun.com/document_detail/86952.html#UserData */
// request.setUserData("{\"MessageCallback\":{\"CallbackURL\":\"http://hicgu6.natappfree.cc/course/list?pageCurrent=1&pageNum=2\"}}");
/* 视频分类ID(可选) */
//request.setCateId(0);
/* 视频标签,多个用逗号分隔(可选) */
//request.setTags("标签1,标签2");
/* 视频描述(可选) */
//request.setDescription("视频描述");
/* 封面图片(可选) */
//request.setCoverURL("http://cover.sample.com/sample.jpg");
/* 模板组ID(可选) */
//request.setTemplateGroupId("8c4792cbc8694e7084fd5330e56a33d");
/* 存储区域(可选) */
//request.setStorageLocation("https://yujia-shanghai-bucket.oss-cn-shanghai.aliyuncs.com/kawa2/course-image/");
// request.setStorageLocation("outin-7908e57a68eb11e9a8d800163e1a625e.oss-cn-shanghai.aliyuncs.com");
request.setStorageLocation(aliyunConfig.getVideoLocation());
/* 开启默认上传进度回调 */
request.setPrintProgress(true);
/* 设置自定义上传进度回调 (必须继承 ProgressListener) */
HttpSession session = httpServletRequest.getSession();
request.setProgressListener(new PutObjectProgressListener(session));
UploadVideoImpl uploader = new UploadVideoImpl();
UploadVideoResponse response = uploader.uploadVideo(request);
//System.out.print("RequestId=" + response.getRequestId() + "\n"); //请求视频点播服务的请求ID
if (response.isSuccess()) {
resultVo.setSuccess(true);
resultVo.setMsg("视频上传成功");
//resultVo.setData("videoId:"+response.getVideoId()+",requestId:"+response.getRequestId());
resultVo.setData("videoId:"+response.getVideoId());
// 视频上传成功之后清除session对象
session.invalidate();
} else {
/* 如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因 */
resultVo.setSuccess(false);
resultVo.setMsg("视频上传失败");
resultVo.setData("VideoId=" + response.getVideoId()+",ErrorCode=" + response.getCode()+",ErrorMessage=" + response.getMessage());
}
return resultVo;
}
阿里云进度回调逻辑:
public class PutObjectProgressListener implements VoDProgressListener {
/**
* 已成功上传至OSS的字节数
*/
private long bytesWritten = 0;
/**
* 原始文件的总字节数
*/
private long totalBytes = -1;
/**
* 本次上传成功标记
*/
private boolean succeed = false;
/**
* 视频ID
*/
private String videoId;
/**
* 图片ID
*/
private String imageId;
// 使用session存储文件上传进度
private HttpSession session;
private int uploadPercent = 0;
//构造方法中加入session
public PutObjectProgressListener(){
}
public PutObjectProgressListener(HttpSession mSession) {
this.session = mSession;
session.setAttribute("upload_percent", uploadPercent);
}
public void progressChanged(ProgressEvent progressEvent) {
long bytes = progressEvent.getBytes();
ProgressEventType eventType = progressEvent.getEventType();
switch (eventType) {
// 开始上传事件
case TRANSFER_STARTED_EVENT:
if (videoId != null) {
System.out.println("Start to upload videoId " + videoId + "......");
}
if (imageId != null) {
System.out.println("Start to upload imageId " + imageId + "......");
}
break;
// 计算待上传文件总大小事件通知,只有调用本地文件方式上传时支持该事件
case REQUEST_CONTENT_LENGTH_EVENT:
this.totalBytes = bytes;
System.out.println(this.totalBytes + "bytes in total will be uploaded to OSS.");
break;
// 已经上传成功文件大小事件通知
case REQUEST_BYTE_TRANSFER_EVENT:
this.bytesWritten += bytes;
if (this.totalBytes != -1) {
int percent = (int) (this.bytesWritten * 100.0 / this.totalBytes);
// 将进度添加到session中
//将进度percent放入session中
session.setAttribute("upload_percent", percent);
System.out.println(bytes + " bytes have been written at this time, upload progress: " +
percent + "%(" + this.bytesWritten + "/" + this.totalBytes + ")");
} else {
System.out.println(bytes + " bytes have been written at this time, upload sub total : " +
"(" + this.bytesWritten + ")");
}
break;
// 文件全部上传成功事件通知
case TRANSFER_COMPLETED_EVENT:
this.succeed = true;
if (videoId != null) {
System.out.println("Succeed to upload videoId " + videoId + " , " + this.bytesWritten + " bytes have been transferred in total.");
}
if (imageId != null) {
System.out.println("Succeed to upload imageId " + imageId + " , " + this.bytesWritten + " bytes have been transferred in total.");
}
break;
// 文件上传失败事件通知
case TRANSFER_FAILED_EVENT:
if (videoId != null) {
System.out.println("Failed to upload videoId " + videoId + " , " + this.bytesWritten + " bytes have been transferred.");
}
if (imageId != null) {
System.out.println("Failed to upload imageId " + imageId + " , " + this.bytesWritten + " bytes have been transferred.");
}
break;
default:
break;
}
}
public boolean isSucceed() {
return succeed;
}
public void onVidReady(String videoId) {
setVideoId(videoId);
}
public void onImageIdReady(String imageId) {
setImageId(imageId);
}
public String getVideoId() {
return videoId;
}
public void setVideoId(String videoId) {
this.videoId = videoId;
}
public String getImageId() {
return imageId;
}
public void setImageId(String imageId) {
this.imageId = imageId;
}
}
请求上传进度接口:
@RequestMapping ("/percent")
@ResponseBody
public int getUploadPercent(HttpServletRequest request){
HttpSession session = request.getSession();
int percent = session.getAttribute("upload_percent") == null ? 0: (Integer)session.getAttribute("upload_percent");
return percent;
}
根据文件上传成功返回的视频id获取视频url接口暂不提供,可参考阿里云api;
4.调用逻辑说明:
调用请求进度接口(确保session对象唯一)–>前端页面调用文件上传接口–>定时请求请求进度接口,实时显示上传进度.–>根据视频id获取视频url接口(视频上传完成之后查看上传视频内容)
5.补充一种使用场景(前后端分离项目)
上面说的解决方案适用于前后端一体项目,如果前后端分离的项目介绍一下我的解决方案:采用redis(此处使用jedis客户端进行存取).直接上代码
文件上传代码:
public ResultVo uploadVideo(String title,String fileName,HttpServletRequest httpServletRequest) {
ResultVo resultVo = new ResultVo();
// 判空处理
if(StringUtils.isEmpty(title)||StringUtils.isEmpty(fileName)){
resultVo.setSuccess(false);
resultVo.setMsg("视频上传失败:视频标题或是视频文件路径为空");
return resultVo;
}
// 文件类型校验
// 对上传的视频格式进行校验: 这里简单校验后缀名
boolean isLegal = false;
for (String type : VIDEO_TYPE) {
if (StringUtils.endsWithIgnoreCase(fileName, type)) {
isLegal = true;
break; // 只要与允许上传格式其中一个匹配就可以
}
}
// 格式错误, 返回与前端约定的error
if (!isLegal) {
resultVo.setSuccess(false);
resultVo.setMsg("视频上传失败:"+"视频格式错误,支持格式:.mp4,.rmvb,.rm,.avi,.flv");
return resultVo;
}
UploadVideoRequest request = new UploadVideoRequest(aliyunConfig.getVideoAccessKeyId(), aliyunConfig.getVideoAccessKeySecret(), title, fileName);
/* 可指定分片上传时每个分片的大小,默认为1M字节 */
//request.setPartSize(1 * 1024 * 1024L);
/* 可指定分片上传时的并发线程数,默认为1,(注:该配置会占用服务器CPU资源,需根据服务器情况指定)*/
//request.setTaskNum(1);
/* 是否开启断点续传, 默认断点续传功能关闭。当网络不稳定或者程序崩溃时,再次发起相同上传请求,可以继续未完成的上传任务,适用于超时3000秒仍不能上传完成的大文件。
注意: 断点续传开启后,会在上传过程中将上传位置写入本地磁盘文件,影响文件上传速度,请您根据实际情况选择是否开启*/
//request.setEnableCheckpoint(false);
/* OSS慢请求日志打印超时时间,是指每个分片上传时间超过该阈值时会打印debug日志,如果想屏蔽此日志,请调整该阈值。单位: 毫秒,默认为300000毫秒*/
//request.setSlowRequestsThreshold(300000L);
/* 可指定每个分片慢请求时打印日志的时间阈值,默认为300s*/
//request.setSlowRequestsThreshold(300000L);
/* 是否使用默认水印(可选),指定模板组ID时,根据模板组配置确定是否使用默认水印*/
//request.setIsShowWaterMark(true);
/* 自定义消息回调设置(可选),参数说明参考文档 https://help.aliyun.com/document_detail/86952.html#UserData */
// request.setUserData("{\"MessageCallback\":{\"CallbackURL\":\"http://hicgu6.natappfree.cc/course/list?pageCurrent=1&pageNum=2\"}}");
/* 视频分类ID(可选) */
//request.setCateId(0);
/* 视频标签,多个用逗号分隔(可选) */
//request.setTags("标签1,标签2");
/* 视频描述(可选) */
//request.setDescription("视频描述");
/* 封面图片(可选) */
//request.setCoverURL("http://cover.sample.com/sample.jpg");
/* 模板组ID(可选) */
//request.setTemplateGroupId("8c4792cbc8694e7084fd5330e56a33d");
/* 存储区域(可选) */
//request.setStorageLocation("https://yujia-shanghai-bucket.oss-cn-shanghai.aliyuncs.com/kawa2/course-image/");
// request.setStorageLocation("outin-7908e57a68eb11e9a8d800163e1a625e.oss-cn-shanghai.aliyuncs.com");
request.setStorageLocation(aliyunConfig.getVideoLocation());
/* 开启默认上传进度回调 */
request.setPrintProgress(true);
/* 设置自定义上传进度回调 (必须继承 ProgressListener) */
request.setProgressListener(new PutObjectProgressListener());
UploadVideoImpl uploader = new UploadVideoImpl();
UploadVideoResponse response = uploader.uploadVideo(request);
//System.out.print("RequestId=" + response.getRequestId() + "\n"); //请求视频点播服务的请求ID
if (response.isSuccess()) {
resultVo.setSuccess(true);
resultVo.setMsg("视频上传成功");
//resultVo.setData("videoId:"+response.getVideoId()+",requestId:"+response.getRequestId());
resultVo.setData("videoId:"+response.getVideoId());
} else {
/* 如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因 */
resultVo.setSuccess(false);
resultVo.setMsg("视频上传失败");
resultVo.setData("VideoId=" + response.getVideoId()+",ErrorCode=" + response.getCode()+",ErrorMessage=" + response.getMessage());
}
return resultVo;
}
进度回调类中将进度添加到缓存中:
public class PutObjectProgressListener implements VoDProgressListener {
/**
* 已成功上传至OSS的字节数
*/
private long bytesWritten = 0;
/**
* 原始文件的总字节数
*/
private long totalBytes = -1;
/**
* 本次上传成功标记
*/
private boolean succeed = false;
/**
* 视频ID
*/
private String videoId;
/**
* 图片ID
*/
private String imageId;
// add by txm 2020/11/19 视频上传进度使用redis方案解决
private Jedis jedis = new Jedis("redisIP", redis端口);
public void progressChanged(ProgressEvent progressEvent) {
long bytes = progressEvent.getBytes();
ProgressEventType eventType = progressEvent.getEventType();
switch (eventType) {
// 开始上传事件
case TRANSFER_STARTED_EVENT:
if (videoId != null) {
System.out.println("Start to upload videoId " + videoId + "......");
}
if (imageId != null) {
System.out.println("Start to upload imageId " + imageId + "......");
}
break;
// 计算待上传文件总大小事件通知,只有调用本地文件方式上传时支持该事件
case REQUEST_CONTENT_LENGTH_EVENT:
this.totalBytes = bytes;
System.out.println(this.totalBytes + "bytes in total will be uploaded to OSS.");
break;
// 已经上传成功文件大小事件通知
case REQUEST_BYTE_TRANSFER_EVENT:
this.bytesWritten += bytes;
if (this.totalBytes != -1) {
int percent = (int) (this.bytesWritten * 100.0 / this.totalBytes);
System.out.println(bytes + " bytes have been written at this time, upload progress: " +
percent + "%(" + this.bytesWritten + "/" + this.totalBytes + ")");
// add by txm 2020/11/19 redis解决视频上传进度问题
jedis.set("progressData",String.valueOf(percent));
} else {
System.out.println(bytes + " bytes have been written at this time, upload sub total : " +
"(" + this.bytesWritten + ")");
}
break;
// 文件全部上传成功事件通知
case TRANSFER_COMPLETED_EVENT:
this.succeed = true;
if (videoId != null) {
System.out.println("Succeed to upload videoId " + videoId + " , " + this.bytesWritten + " bytes have been transferred in total.");
}
if (imageId != null) {
System.out.println("Succeed to upload imageId " + imageId + " , " + this.bytesWritten + " bytes have been transferred in total.");
}
break;
// 文件上传失败事件通知
case TRANSFER_FAILED_EVENT:
if (videoId != null) {
System.out.println("Failed to upload videoId " + videoId + " , " + this.bytesWritten + " bytes have been transferred.");
}
if (imageId != null) {
System.out.println("Failed to upload imageId " + imageId + " , " + this.bytesWritten + " bytes have been transferred.");
}
break;
default:
break;
}
}
public boolean isSucceed() {
return succeed;
}
public void onVidReady(String videoId) {
setVideoId(videoId);
}
public void onImageIdReady(String imageId) {
setImageId(imageId);
}
public String getVideoId() {
return videoId;
}
public void setVideoId(String videoId) {
this.videoId = videoId;
}
public String getImageId() {
return imageId;
}
public void setImageId(String imageId) {
this.imageId = imageId;
}
}
控制层获取缓存中进度的逻辑:
private Jedis jedis = new Jedis("redis服务器IP", redis服务器端口);
@RequestMapping ("/percent")
@ResponseBody
public ResultVo getUploadPercent(HttpServletRequest request){
String progressData = jedis.get("progressData");
return ResultVoUtil.success(progressData,"获取上传进度成功");
}
前后端分离的项目进行数据共享的方式还有很多,有时间会进行更新说明.