前言
- oss的文件下载进度条功能官方是提供了各种语言的SDK,但是想要返回数据给前端显示使用那是不可能的还需要进行修改,楼主项目中后面的模块上传和下载会用到大文件上传下载,所以提前研究一下官方的进度条显示功能。
- 官方SDK地址: 阿里云oss下载进度条
实现思路
OSS中每一次上传都会生成一个taskId相当于上传任务的id,在创建oss客户端时可以将自己实现的ProgressListener添加到客户端中,它可以实时的去显示你传入的任务id上传进度(想要在代码中查看进度的可以打开我分享的代码中log.info("***下载进度: {}***", percent + "%(" + this.bytesRead + "/" + this.totalBytes + ")");
这个代码来查看后台上传过程中的日志打印,lz自己做的时候关闭了这些日志。),但是由于我们访问上传请求时,oss的上传接口它会处于等待文件上传的状态,会阻塞住导致我们无法去立即响应当前http请求的结果,所以我们需要将oss上传任务放入到线程池中,做一个异步操作,然后想办法在这个生成taskId时就传出去并响应给前端,然后前端拿到这个taskId后每隔20ms请求另外一个接口,这个接口专门负责查询taskId的下载进度,这样的话就实现了进度条的功能。具体代码实现思路的话楼主代码注释十分详细,可以耐心查看,毕竟实现功能都是要时间的
- 创建类实现ProgressListener接口: 创建GetObjectProgressListener类实现ProgressListener接口,重写progressChanged(ProgressEvent progressEvent)方法,该方法会重复回调显示下载事件的进度(频率很高),然后通过成员变量bytesRead记录下载的字节数,使用percent记录当前进度,代表百分之percent,taskId用于记录当前下载任务的唯一标识,在进入到 TRANSFER_STARTED_EVENT 事件之中时赋值,GetObjectProgressListener用于监听下载任务的进度,对象中主要包含以下成员变量。
- 创建AsyncMethod类封装oss下载的函数,用于异步调用方法: 创建AsyncMethod类封装oss下载的方法(一定要创建类,不然后面异步调用的时候会失效),方法中主要实现oss的带有进度条下载,传入GetObjectProgressListener对象用于记录下载进度
- 创建对应接口用于调用oss下载进度条函数: 下载功能都是web请求下载,所以为了测试功能创建对应接口来调用测试
- 开通springboot的异步调用机制: 由于oss创建下载任务(如下代码)时,会进入阻塞状态,所以我们需要异步调用AsyncMethod中的progressBarDownload方法。而springboot默认未开启异步调用,所以需要开启springboot的异步调用机制
ossClient.getObject(
new GetObjectRequest(bucketName, objectName).
withProgressListener(progressListener),
new File(pathname));
-
配置同步参数类AsyncConfig,其中主要是添加线程池操作
-
启动类中添加@EnableAsync注解,代表开启异步调用
-
需要异步调用的方法上添加@Async注解
-
浏览器中访问接口,创建下载任务并且报保存下载进度,percent代表下载进度55%,一直刷新的话就会发现percent一直增大,等到100%的时候代表下载完成,不过到了100%时taskId会销毁需要另外处理一下
-
查看后台日志输出
相关代码实现
- oss依赖和版本
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
- 创建GetObjectProgressListener,实现ProgressListener.里面需要配置accessKeyId和accessKeySecret。响应字节转换事件中的日志可以打开,查看下载过程
package com.luntek.commons.utils.oss;
import com.aliyun.oss.event.ProgressEvent;
import com.aliyun.oss.event.ProgressEventType;
import com.aliyun.oss.event.ProgressListener;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* OSS下载进度条功能,用于记录指定下载任务的下载进度
*
* @author: Czw
* @create: 2020-10-26 13:41
**/
@Slf4j
@Lazy
@Component
@Data
public class GetObjectProgressListener implements ProgressListener {
//已经存好的数据长度
private long bytesRead = 0;
//下载的数据总长度
private long totalBytes = -1;
//下载的当前进度,例如50时,代表下载进度为50%
private int percent = 0;
//是否下载完成
private boolean succeed = false;
//数据在oss中的objectName,也相当于任务ID
private String taskId = "";
// Endpoint以杭州为例,其它Region请按实际情况填写。
private static final String endpoint = "******";
// RAM账号进行API访问或日常运维,请登录RAM控制台创建RAM账号。
private static final String accessKeyId = "******";
private static final String accessKeySecret = "******";
//用来存储所有下载任务进度
public static Map<String,GetObjectProgressListener> progressBarMap=new HashMap<>();
@Override
public void progressChanged(ProgressEvent progressEvent) {
long bytes = progressEvent.getBytes();
ProgressEventType eventType = progressEvent.getEventType();
switch (eventType) {
case TRANSFER_STARTED_EVENT:
//响应开始下载事件
String taskId = UUID.randomUUID().toString();
this.taskId = taskId;
//保存下载任务
progressBarMap.put(taskId,this);
log.info("***开始下载,progressChanged中的taskId={}***", this.taskId);
break;
case RESPONSE_CONTENT_LENGTH_EVENT:
//响应下载的字节长度事件
this.totalBytes = bytes;
log.info("***响应下载的字节长度事件***");
log.info("【{}】 bytes in total will be downloaded to a local file", this.totalBytes);
break;
case RESPONSE_BYTE_TRANSFER_EVENT:
//响应字节转换事件
this.bytesRead += bytes;
if (this.totalBytes != -1) {
// log.info("***响应字节转换事件***");
int percent = (int) (this.bytesRead * 100.0 / this.totalBytes);
this.percent = percent;
// log.info("***下载进度: {}***", percent + "%(" + this.bytesRead + "/" + this.totalBytes + ")");
} else {
log.info(bytes + " bytes have been read at this time, download ratio: unknown" +
"(" + this.bytesRead + "/...)");
}
break;
case TRANSFER_COMPLETED_EVENT:
//下载完成事件
this.succeed = true;
//移除任务
progressBarMap.remove(this.taskId);
log.info("下载完成, " + this.bytesRead + " bytes have been transferred in total");
break;
case TRANSFER_FAILED_EVENT:
//下载失败事件
log.info("下载失败, " + this.bytesRead + " bytes have been transferred");
break;
default:
break;
}
}
}
- 线程池异步配置AsyncConfig(已经有的可以跳过)
package com.luntek.commons.async;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 异步线程池
*
* @author: Czw
* @create: 2020-10-27 18:30
**/
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor myAsync() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//获取电脑核数
int core = Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(core);
executor.setMaxPoolSize(core * 2 + 1);
executor.setQueueCapacity(8192);
//线程名称前缀
String threadNamePrefix = "MyExecutor-";
executor.setThreadNamePrefix(threadNamePrefix);
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
- 封装异步方法AsyncMethod(不要懒放在其他类中,一定要加,不听劝的也可以试试看)
package com.luntek.commons.async;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.GetObjectRequest;
import com.luntek.commons.utils.oss.GetObjectProgressListener;
import com.luntek.commons.utils.oss.OSSUtil;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.io.File;
/**
* 异步方法
* @author: Czw
* @create: 2020-10-27 20:12
**/
@Component
@Lazy
public class AsyncMethod {
/**
* oss带有进度条功能下载
*
* @param progressListener 当前下载任务对象
* @param bucketName 存储名称
* @param objectName 文件夹路径+文件名称,例如abc/efg/123.jpg
* @param pathname oss下载下来的文件临时保存路径
*/
@Async(value = "myAsync")
public void progressBarDownload(GetObjectProgressListener progressListener,
String bucketName,
String objectName,
String pathname) {
OSS ossClient = new OSSClientBuilder().build("YouEndpoint","YourAccessKeyId","YourAccessKeySecret");
// 下载文件的同时指定了进度条参数。
ossClient.getObject(
new GetObjectRequest(bucketName, objectName).
withProgressListener(progressListener),
new File(pathname));
// 关闭OSSClient。
ossClient.shutdown();
}
}
- 创建调用的api接口和service层
controller
//通过下载任务的taskId获取下载进度
@GetMapping("/getDownloadProgress")
public ResponseResult<?> getDownloadProgress(String taskId) {
log.info("***getDownloadProgress***");
//返回的数据为空时,代表下载任务未创建或者已经下载完成
//若想区分状态可以在GetObjectProgressListener类中添加status属性记录
return ResponseResult.success(GetObjectProgressListener.progressBarMap.get(taskId));
}
//异步调用oss下载带有进度条方法,创建下载任务
@Resource
private TestService testService;
@GetMapping("/testOSSSAsynchronous")
public ResponseResult<?> testOSSSAsynchronous() {
log.info("***testOSSSAsynchronous***");
GetObjectProgressListener progressListener = testService.testOSSAsynchronous();
try {
//通过调整休眠时间来查看下载进度
//第一次生成下载任务返回调用时也需要休眠200毫秒左右,确保下载进度进入到TRANSFER_STARTED_EVENT事件生成taskId中
Thread.sleep(3000);
log.info("***Thread.sleep***");
} catch (InterruptedException e) {
e.printStackTrace();
}
return ResponseResult.success(progressListener);
}
TestService
GetObjectProgressListener testOSSAsynchronous();
TestServiceImpl
@Override
public GetObjectProgressListener testOSSAsynchronous() {
GetObjectProgressListener objectListener = new GetObjectProgressListener();
asyncMethod.progressBarDownload(
objectListener,
"luntek-resource-cloud-homework-practice",
"test/navicat_39234.zip",
"C:\\Users\\Administrator\\Desktop\\navicat_39234.zip");
log.info("下载完成返回的GetObjectProgressListener ID=【{}】,percent=【{}】",
objectListener.getTaskId(), objectListener.getPercent());
return objectListener;
}
-
启动类添加注解
@EnableAsync
//扫描类下异步方法
-
项目包结构
测试效果
测试的数据167M左右,下载速度一般在2-4M/S,我们在代码中设置异步调用之后的等待时间,然后返回数据
设置下载的文件
设置下载文件
oss客户端上查看到的文件信息
设置等待时间
前端效果
- 生成taskId
- 查看任务下载进度
** *基本上下载的代码都贡献出来了,资料整理了4小时,最后希望大家点个赞* **
** *创作不易,转发注明出处* **
相关问题
- 日志打印: 引用的sdk中会使用阿帕奇相关方法,所以需要设置org.apache.http.wire的相关日志级别,具体可以查看这篇帖子:配置文件logback-spring.xml