項目需求
導出生成大批量數據的文件,一個Excel中最多存有五十萬條數據,查詢多余五十萬的數據寫多個Excel中。導出完成是生成的多個Excel文件打包壓縮成zip,而后更新導出記錄中的壓縮文件路徑。
大數據量文件一般采用異步生成文件,導出時首先授權生成一個流水號,而后將數據攜帶流水號請求導出接口。
拋開實際業務,做成一個比較公共的導出功能。
參數說明
{
"className": "ValideData", //導出的數據的實體類,類中有別名和順序相關的注解
"createUser": "", //操作人
"downLoadNo": "202203181504732568468066304", //下載流水號
"fileName": "機卡綁定", //文件名 fileName+HHmmssSSS.xlsx
"keys": [ //redis key的數據,分批獲取數據
],
"remark": "機卡綁定", //備注(不關注)
"type": "機卡綁定" //導出類型(不關注)
}
坐標
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.21</version>
</dependency>
注:拋開導出前的參數校驗,只關注導出操作 。
主要代碼
邏輯說明:
- 導出前將請求參數更新到導出記錄中。
- 類加載器加載需要導出數據的實體類
- 設置一個數據量指針,記錄到每個文件的數據量
- 達到閾值時指定文件寫出到磁盤並清緩。
- 重置數據量指針,新增一條文件記錄(循環)
- 數據量指針未到閾值時但數據已經查詢完成---->>寫入剩余數據
- 查詢該流水號的所有文件記錄
- 壓縮文件並返回壓縮文件地址
- 更新到導出記錄中
主流程
public void bigDataExport(PortDto dto) throws Exception {
long start = System.currentTimeMillis();
log.info("開始導出,批次號:<{}>, 開始時間:{}", dto.getDownLoadNo(), DateUtil.now());
//修改導出記錄
LambdaUpdateWrapper<PortDto> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(PortDto::getDownLoadNo, dto.getDownLoadNo());
//生成導出記錄
int row = this.baseMapper.update(dto, updateWrapper);
if (row > 0) {
log.info("批次號:<{}>准備生成文件", dto.getDownLoadNo());
try {
Iterator<String> iterator = keys.iterator();
Workbook workbook = null;
ExportParams params = new ExportParams();
//加載導出數據實體類
Class<?> aClass = Class.forName(entityBasePackage + dto.getClassName());
int element = 0;
while (iterator.hasNext()) {
String key = iterator.next();
Collection<?> list = getList(key, aClass);
element += list.size();
workbook = ExcelExportUtil.exportBigExcel(params, aClass, list);
//文件數據達到閾值
if (element >= maxDataCount) {
String fileName = dto.getFileName() + "_" + DateUtil.format(new Date(),
"HHmmssSSS") + ".xlsx";
ExcelExportUtil.closeExportBigExcel();
FileOutputStream fos =
new FileOutputStream(fileProp.getPath().getPath() + fileName);
workbook.write(fos);
fos.close();
element = 0;
//更新地址
Map<String, Object> map = new HashMap<>();
map.put("downloadNo", dto.getDownLoadNo());
map.put("filePath", fileProp.getPath().getPath() + fileName);
map.put("createTime", new Date());
this.baseMapper.insertPathRecord(map);
log.info("文件寫入完成,文件名:{}", fileName);
continue;
}
iterator.remove();
}
//寫入剩余文件
if (element != 0) {
String fileName = dto.getFileName() + "_" + DateUtil.format(new Date(),
"HHmmssSSS") + ".xlsx";
ExcelExportUtil.closeExportBigExcel();
FileOutputStream fos = new FileOutputStream(fileProp.getPath().getPath() + fileName);
workbook.write(fos);
fos.close();
element = 0;
//更新地址
Map<String, Object> map = new HashMap<>();
map.put("downloadNo", dto.getDownLoadNo());
map.put("filePath", fileProp.getPath().getPath() + fileName);
map.put("createTime", new Date());
this.baseMapper.insertPathRecord(map);
log.info("文件寫入完成,文件名:{}", fileName);
}
long end = System.currentTimeMillis();
log.info("導出結束,批次號:<{}>, 結束時間:{}, 耗時:{}", dto.getDownLoadNo(), DateTime.of(end),
DateUtil.formatBetween(end - start));
} catch (Exception e) {
log.info("批次號<{}>導出異常:", dto.getDownLoadNo(), e);
throw new BusinessException("");
} finally {
log.info("批次號<{}>生成文件結束,准備壓縮文件,修改狀態", dto.getDownLoadNo());
//合並文件到導出文件記錄主表
//當只有一個文件記錄時直接更新主表文件地址
List<PortDto> recordList = exportDao.getPathRecord(dto);
if (recordList.size() > 1) {
//zipPath
dto.setFilePath(zcat(dto, recordList));
} else {
//xlsxPath
dto.setFilePath(recordList.size()==0? "":recordList.get(0).getFilePath());
}
updateWrapper.clear();
updateWrapper.set(PortDto::getFilePath, dto.getFilePath());
updateWrapper.set(PortDto::getSuccessTime, new Date());
updateWrapper.set(PortDto::getStatus, "1");
updateWrapper.eq(PortDto::getDownLoadNo, dto.getDownLoadNo());
this.baseMapper.update(null, updateWrapper);
log.info("批次號<{}>更新下載記錄表文件地址,修改狀態成功", dto.getDownLoadNo());
}
}
}
文件壓縮
/**
* 多文件壓縮
* @param dto 導出信息
* @Param recordList 文件路徑
* @return void
* @throws
* @author Surpass
* @date 2022/3/17 9:59
*/
private String zcat(PortDto dto, List<PortDto> recordList) throws Exception {
String fileName = dto.getFileName() + "_" + DateUtil.format(new Date(), "HHmmssSSS") + ".zip";
String zipPath = fileProp.getPath().getPath() + fileName;
Archiver archiver = CompressUtil.createArchiver(
CharsetUtil.CHARSET_UTF_8,
ArchiveStreamFactory.ZIP,
new File(zipPath)
);
for (PortDto portDto : recordList) {
archiver.add(FileUtil.file(portDto.getFilePath()));
}
archiver.finish();
archiver.close();
return zipPath;
}
查詢數據
/**
* 查詢redis數據
* @param key
* @param cls
* @return java.util.Collection<?>
* @throws
* @author Surpass
* @date 2022/3/18 15:51
*/
private Collection<?> getList(String key, Class<?> cls) {
List<String> list = redis.getList(key);
return list.stream().map(item -> JSONObject.parseObject(item, cls)).collect(Collectors.toList());
}
補充
導出還設置了隊列計數器來限制同一時間最大的導出請求,使用aop在申請流水號時計數器+1,導出完成或者異常時隊列計數器-1。導出完成后根據操作人發送郵件通知導出結果。
