手把手教你簡(jiǎn)單實(shí)現(xiàn)一個(gè)導(dǎo)出文件下載進(jìn)度條
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來,我們一起精進(jìn)!你不來,我和你的競(jìng)爭(zhēng)對(duì)手一起精進(jìn)!
編輯:業(yè)余草
來源:juejin.cn/post/7261741840829874235
推薦:https://t.zsxq.com/10kIjsBVG
自律才能自由
前言
今天要跟大家分享的是一個(gè)導(dǎo)出數(shù)據(jù)進(jìn)度條的簡(jiǎn)單實(shí)現(xiàn),適用場(chǎng)景用在數(shù)據(jù)量大、組織數(shù)據(jù)耗時(shí)的情況下的簡(jiǎn)單實(shí)現(xiàn)。
設(shè)計(jì)思路
-
導(dǎo)出數(shù)據(jù)生成文件上傳到 OSS -
導(dǎo)出數(shù)據(jù)狀態(tài)存 redis 緩存 -
前端發(fā)導(dǎo)出請(qǐng)求后,返回的文件 key -
請(qǐng)求后端,后端查詢緩存情況返回 -
前端解析是否完成標(biāo)值,如果完成結(jié)束輪詢,執(zhí)行下載 get 下載,如果未完成,等待下一次輪詢
設(shè)計(jì)時(shí)序圖
核心代碼
導(dǎo)出請(qǐng)求
下載請(qǐng)求
/**
* 因子達(dá)標(biāo)分析匯總表導(dǎo)出
*
* @param airEnvQualityQueryVo 因子達(dá)標(biāo)分析匯總表導(dǎo)出
* @return 統(tǒng)一出參
*/
@PostMapping("/propSummaryData/export")
@ApiOperation("因子達(dá)標(biāo)分析匯總表導(dǎo)出")
public RestMessage propSummaryData4Export(@RequestBody AirEnvQualityQueryVo airEnvQualityQueryVo) {
Assert.notNull(airEnvQualityQueryVo, "查詢參數(shù)不能為空");
Assert.notNull(airEnvQualityQueryVo.getStartTime(), "開始時(shí)間不能為空");
Assert.notNull(airEnvQualityQueryVo.getEndTime(),"結(jié)束時(shí)間不能為空");
Assert.isTrue(StringUtils.isNotBlank(airEnvQualityQueryVo.getQueryType()),"查詢類型不能為空");
SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss");
String key = "propSummaryData:"+formatter.format(new Date());
AsyncUtil.submitTask(key,() ->{
//獲取并組織excel數(shù)據(jù)
String url;
try {
url = airEnvironmentExportService.propSummaryData4Export(airEnvQualityQueryVo,key);
} catch (Exception e) {
throw new BusinessException(e.getMessage());
}
return url;
});
return RestBuilders.successBuilder().data(key).build();
}
「serviceImpl」
/**
* 因子達(dá)標(biāo)分析匯總表導(dǎo)出
*
* @param airEnvQualityQueryVo 因子達(dá)標(biāo)分析匯總表導(dǎo)出
* @return 統(tǒng)一出參
*/
@Override
public String propSummaryData4Export(AirEnvQualityQueryVo airEnvQualityQueryVo, String key) throws IOException {
//獲取匯聚數(shù)據(jù)
AirEnvQualityResultOverviewVo resultOverviewVo = airEnvironmentQualityStatisticsService.getAirEnvQualityResultOverviewVo(airEnvQualityQueryVo);
//數(shù)據(jù)轉(zhuǎn)換
resultOverviewVo.setTqRateCompStr(rateHandlerStr(resultOverviewVo.getTqRateComp()));
resultOverviewVo.setSqRateCompStr(rateHandlerStr(resultOverviewVo.getSqRateComp()));
//獲取或者數(shù)據(jù)
List<AirEnvQualityPropSummaryVo> airEnvQualityPropSummaryVos = airEnvironmentQualityStatisticsService.propSummaryData(airEnvQualityQueryVo);
AtomicInteger done = new AtomicInteger();
AsyncUtil.setTotal(key,airEnvQualityPropSummaryVos.size());
airEnvQualityPropSummaryVos.forEach(vo ->{
//數(shù)據(jù)轉(zhuǎn)換
vo.setBqReachRateStr(rateHandler(vo.getBqReachRate()));
vo.setTqReachRateCompStr(rateHandlerStr(vo.getTqReachRateComp()));
vo.setSqReachRateCompStr(rateHandlerStr(vo.getSqReachRateComp()));
vo.setBqExceedRateStr(rateHandler(vo.getBqExceedRate()));
vo.setTqExceedRateCompStr(rateHandlerStr(vo.getTqExceedRateComp()));
vo.setSqExceedRateCompStr(rateHandlerStr(vo.getSqExceedRateComp()));
done.getAndIncrement();
AsyncUtil.setDone(key,done.get());
});
//組織導(dǎo)出數(shù)據(jù)
Map<String,Object> map = new HashMap<>();
map.put("p",resultOverviewVo);
map.put("w",airEnvQualityPropSummaryVos);
String url = getExcelUrl(map, "propSum.xlsx", "因子分析匯總");
return url;
}
核心工具類
AsyncUtil 負(fù)責(zé)異步更新生成文件數(shù)據(jù)組織情況更新,存儲(chǔ)到緩存。
import cn.hutool.core.collection.CollectionUtil;
import com.easylinkin.oss.OSSBaseService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
@Component
public class AsyncUtil implements ApplicationContextAware {
static Logger LOG = LoggerFactory.getLogger(AsyncUtil.class);
public static ExecutorService executor = Executors.newFixedThreadPool(40);
public static ScheduledExecutorService ex = Executors.newScheduledThreadPool(1);
static List<String> keys = new ArrayList<>();
static boolean scheduleIsStart = false;
private static OSSBaseService ossService;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ossService = applicationContext.getBean(OSSBaseService.class);
}
public static RedisTemplate<String, RedisAsyResultData> getRedisTemplate() {
return SpringUtils.getBean("redisTemplate", RedisTemplate.class);
}
static void updateKeyLiveTime() {
if (!scheduleIsStart) {
// 更新redis中緩存的過期時(shí)間
ex.scheduleAtFixedRate(() -> {
try {
LOG.info("----- update AsyncResult keys length:{} -----",
keys.size());
if (CollectionUtil.isNotEmpty(keys)) {
List<RedisAsyResultData> multiGet =
getRedisTemplate().opsForValue().multiGet(keys);
for (RedisAsyResultData result : multiGet) {
if (result != null) {
String key = result.getRedisKey();
getRedisTemplate()
.expire(key, 5, TimeUnit.MINUTES);
}
}
}
} catch (Exception e) {
scheduleIsStart = false;
LOG.error(e.getMessage(), e);
}
}, 1, 3, TimeUnit.MINUTES);
scheduleIsStart = true;
}
}
public static RedisAsyResultData submitExportTask(String key, Supplier supplier) {
RedisAsyResultData rs = new RedisAsyResultData();
rs.setSuccess(false);
rs.setRedisKey(key);
rs.setDone(0);
rs.setTotal(100);
setToRedis(rs, key);
if (!keys.contains(key)) {
keys.add(key);
}
String finalKey = key;
executor.submit(() -> {
String msg = null;
try {
Object o = supplier.get();
rs.setData(o);
rs.setFlag(true);
} catch (Exception e) {
rs.setFlag(false);
msg = e.getMessage();
LOG.error(e.getMessage(), e);
}
rs.setSuccess(true);
rs.setDone(rs.getTotal());
if (null != msg) {
rs.setError(msg);
}
keys.remove(finalKey);
setToRedis(rs, finalKey);
});
updateKeyLiveTime();
return rs;
}
/**
* 設(shè)置進(jìn)度
* @param key
* @param done
* @return
*/
public static void setDone(String key,Integer done){
RedisAsyResultData result = getResult(key);
Optional.ofNullable(result).ifPresent(re -> {
re.setDone(done);
saveResult(key,result);
});
}
/**
* 設(shè)置總數(shù)
* @param key
* @param total
* @return
*/
public static void setTotal(String key,Integer total){
RedisAsyResultData result = getResult(key);
Optional.ofNullable(result).ifPresent(re -> {
re.setTotal(total);
saveResult(key,result);
});
}
public static RedisAsyResultData submitTask(String key, Supplier supplier) {
AtomicReference<RedisAsyResultData> rs = new AtomicReference<>(new RedisAsyResultData());
rs.get().setSuccess(false);
rs.get().setRedisKey(key);
rs.get().setDone(0);
rs.get().setTotal(100);
setToRedis(rs.get(), key);
if (!keys.contains(key)) {
keys.add(key);
}
String finalKey = key;
executor.submit(() -> {
String msg = null;
try {
Object o = supplier.get();
RedisAsyResultData result = getResult(key);
if (null != result){
rs.set(result);
}
rs.get().setData(o);
rs.get().setFlag(true);
} catch (Exception e) {
rs.get().setFlag(false);
msg = e.getMessage();
LOG.error(e.getMessage(), e);
}
rs.get().setSuccess(true);
rs.get().setDone(rs.get().getTotal());
if (null != msg) {
rs.get().setError(msg);
}
keys.remove(finalKey);
setToRedis(rs.get(), finalKey);
});
updateKeyLiveTime();
return rs.get();
}
private static void setToRedis(RedisAsyResultData result, String redisKey) {
getRedisTemplate().opsForValue().set(redisKey, result, 5, TimeUnit.MINUTES);
}
public static RedisAsyResultData getResult(String key) {
RedisAsyResultData excelResult =
getRedisTemplate().opsForValue().get(key);
if (null != excelResult) {
return excelResult;
}
return null;
}
public static void saveResult(String key, RedisAsyResultData result) {
setToRedis(result, key);
}
public static byte[] FileToByte(String filePath) throws Exception{
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
fis = new FileInputStream(filePath);
bis = new BufferedInputStream(fis);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int c = bis.read();
while (c != -1) {
// 數(shù)據(jù)存儲(chǔ)到ByteArrayOutputStream中
baos.write(c);
c = bis.read();
}
fis.close();
bis.close();
// 轉(zhuǎn)換成二進(jìn)制
byte[] bytes = baos.toByteArray();
return bytes;
}catch (Exception e){
e.printStackTrace();
throw e;
}finally {
try {
if (fis != null ) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
try {
if (bis != null ) {
bis.close();
}
} catch (IOException e) {
e.printStackTrace();
throw e;
}
}
}
}
}
查詢導(dǎo)出文件生成情況接口
/**
* 根據(jù)key獲取導(dǎo)出接口
* @param key
* @return
*/
@GetMapping("getRedisResult/{key}")
public RestMessage getRedisResult(@PathVariable String key){
Assert.hasLength(key,"key不能為空");
return RestBuilders.successBuilder().data(AsyncUtil.getResult(key)).build();
}
key 為導(dǎo)出請(qǐng)求返回的。
效果
前端進(jìn)度條由每一次輪詢請(qǐng)求返回的 total、done 計(jì)算。
最后一次輪詢,判斷 flag 的值 true,或者自行判斷 total 與 done 相等,又或者判斷 data 是否又返回 url 表示是否生成完成,然后用返回的 url 進(jìn)行 get 請(qǐng)求執(zhí)行下載。
總結(jié)
簡(jiǎn)單的實(shí)現(xiàn)進(jìn)度條,用在數(shù)據(jù)需要長(zhǎng)時(shí)間一條條生成時(shí),看進(jìn)度條特別明顯 輪詢其實(shí)也可以用 websocket 替代(這樣可以離開頁面做其他操作,當(dāng)然這樣也是可以的,就是輪詢要做到全局請(qǐng)求了,業(yè)務(wù)模塊多的下載的時(shí)候前后端都?jí)毫ψ兇螅?這里其實(shí)還用到了 easypoi 的模板導(dǎo)出,大家可以自己看看 api。

更多學(xué)習(xí)內(nèi)容來自如下星球,歡迎更多的人加入!
