- InputStream is = files[i].getInputStream();
- XSSFWorkbook xssFWorkbook = new XSSFWorkbook(is);
在02处直接就会内存溢出了。无论怎么抢救都没用,看来得要换一条路走走了。
在不停的Google查资料折腾了两天之后,在POI官网成功的找到了解决方案。此处请允许我稍微吐槽一下POI,有瓶颈问题的解决方案却隐藏的很深,只有一个不起眼的小链接,难道是怕大家都知道有点丢脸?
总结一下该方案的主要原理:超大数据量的excel文件通过页面上传后(nginx的默认最大文件字节要调大一些:client_max_body_size xxxm),后台接收到该文件的对象CommonsMultipartFile。首先获取该文件对象的inputStream,然后用OPCPackage来打开该文件流,将excel在内存中转化为xml格式却不会出现内存溢出的问题,根据该xml的标签就可以识别是格式,标题还是内容。然后在内存中通过POI框架中的XSSFSheetXMLHandler类中的sheetContentsHandler接口来根据上述的标签来解析内容。可以将解析到的内容存入list或者map容器中便于后续的业务处理(纯内容数据,几百万的数据量轻松胜任,亲测不会内存溢出)。当然根据业务需要,需要复写sheetContentsHandler接口的startRow,endRow,cell,headerFooter四大方法。
当然了,笔者也亲测了另一种方案:就是用OPCPackage来打开该文件流并且将excel在内存中转化为xml格式之后,一股脑儿的用缓冲流分批的将所有原excel内容数据写入到本地的txt文件,再去分批的readLine该文件中的数据,进行业务处理。该方案的好处是交易导入的文件可以物理的落地,作为后期查验的依据和凭证。当然了,缺点是多了一次物理转储的过程,耗时会延长。如何选择看个人的业务需求咯。
本文中重点讲述第一种方案吧,话不多说,贴代码:
/**
* @return
* @throws Exception
* @author jason.gao
* 功能描述:交易导入
*/
@RequestMapping(value = "/transDetail/upload", method = {RequestMethod.POST, RequestMethod.GET})
@RequestGuard(perm = "transaction.import.upload")
public ResponseEntity<ResponseEnvelope<RestApiResp>> uploadFile(@RequestParam("file") CommonsMultipartFile[] files, HttpServletRequest req, HttpServletResponse resp) throws IOException {
logger.info("uploadFile == >upload button start; fileName:[{}], CommonsMultipartFile[]:[{}]", files[0].getFileItem().getName(), files);
long start = System.currentTimeMillis();
String result = "完成交易文件的导入!";
if (null == files || files.length != 1) {
return RestApiResp.getSuccResponseEntity("必须上传一个文件", null);
}
//重置buffer,在可能会超时的地方输出resp的字节,避免前端ajax请求断开!
resp.setBufferSize(1);
ServletOutputStream out = resp.getOutputStream();
XlsxProcessAbstract xlsxProcess = new XlsxProcessAbstract();
long getFileAndDataTime;
ProcessTransDetailDataDto data;
try {
//获取明细行累积的支付/退款的总金额/总笔数等数据的DTO
data = xlsxProcess.processAllSheet(files[0]);
logger.info("汇总行的数据:[{}]", data.dtoToString());
//获取汇总行和明细行数据(包含标题)
List<String> contentList = data.contentList;
logger.info("明细行的数据条数为:[{}]", JSON.toJSONString(contentList.size() - 3));
getFileAndDataTime = System.currentTimeMillis();
logger.info("获取文件并得到数据完成。耗时:[{}]秒", (getFileAndDataTime - start)/1000);
//校验汇总行数据正确性
checkDetailSummary(contentList, data, out);
logger.info("汇总行数据正确性的校验已通过!");
//分批调用OSP插入过程
String handleResult = doOspHandle(contentList, data, out);
if (!handleResult.equals(TransImportJobStatus.Success.getValue())) {
result = TransImportJobStatus.getDescByKey(handleResult);
logger.error(result);
}
} catch (CellDataException e) {
logger.error("CellDataException: Error:[{}]", e);
return RestApiResp.getSuccResponseEntity(e.getMessage(), null);
} catch (OspException e) {
logger.error("OspException:[{}]", e);
return RestApiResp.getSuccResponseEntity(e.getMessage(), null);
} catch (IOException e) {
logger.error("IOException:[{}]", e);
return RestApiResp.getSuccResponseEntity(e.getMessage(), null);
} catch (Exception e) {
logger.error("未知异常:[{}]", e);
return RestApiResp.getSuccResponseEntity("未知异常,请排查日志:" + e.getMessage(), null);
}
long finishCheckAndInsertTime = System.currentTimeMillis();
logger.info("完成数据校验和数据分批插入。耗时:[{}]秒", (finishCheckAndInsertTime - getFileAndDataTime)/1000);
logger.info("[{}],整个后台处理过程共耗时:[{}]秒", result, (finishCheckAndInsertTime - start)/1000);
return RestApiResp.getSuccResponseEntity(result, HttpStatus.OK);
}
上面代码块是整个后台的主流程,注意的是要充分的捕捉异常,将异常信息呈献给前端页面和日志系统,便于生产故障时排查问题。
接下来的四个代码块是对excel中字段的业务处理,属于业务部分,不关心业务的可以忽略这些代码片段。
public String doOspHandle (List<String> contentList, ProcessTransDetailDataDto data, ServletOutputStream out) throws CellDataException, OspException, IOException{
// 获取当前工作薄的明细行
int start = 3;
int size = 1000;
String importStatus = "";
//分批调用OSP接口执行插入
while(start < contentList.size()) {
importStatus = handleTransImport(contentList, start, size, data);
if (!importStatus.equals(TransImportJobStatus.Success.getValue())) {
logger.error("从第[{}]到[{}]行的数据,分批调用OSP接口失败", start + 1, start + size + 1);
return importStatus;
}
start += size;
out.write(new String(" ").getBytes());
out.flush();
}
//最终状态:交易全部成功
if (importStatus.equals(TransImportJobStatus.Success.getValue())){
logger.info("调用“交易明细导入”的OSP接口成功!");
TransDetailResp confirmResp;
OspTransDetailServiceHelper.OspTransDetailServiceClient ospTransDetailServiceClient = new OspTransDetailServiceHelper.OspTransDetailServiceClient();
logger.info("调用“确认交易明细成功”的OSP接口的请求参数:商户号=[{}],结算单号=[{}],总条数=[{}]", data.getMerchantId(), data.getSettleOrderNo(), contentList.size()-3);
try{
confirmResp = ospTransDetailServiceClient.transDetailConfirm(data.getMerchantId(), data.getSettleOrderNo(), contentList.size()-3);
} catch (OspException e) {
logger.error("调用“确认交易明细成功”的OSP接口的抛出异常![{}]", e);
throw e;
} finally {
out.write(new String("").getBytes());
out.flush();
}
logger.info("调用“确认交易明细成功”的OSP接口的返回参数为:{}", JSON.toJSONString(confirmResp));
if (!confirmResp.getResponseCode().equals(MessageEnum.SUCCESS.getValue())) {
throw new OspException(TransImpFileExceptEnums.OspTransDetailConfirm.getValue(), TransImpFileExceptEnums.OspTransDetailConfirm.getDesc());
}
}
return importStatus;
}
/**
* 调用osp接口:执行交易明细的导入
* 返回OSP操作完成的状态
*/
public String handleTransImport(List<String> contentList, int start, int size, ProcessTransDetailDataDto data) throws CellDataException, OspException{
//分批的调用osp接口:执行交易明细的导入
OspTransDetailServiceHelper.OspTransDetailServiceClient ospTransDetailServiceClient = new OspTransDetailServiceHelper.OspTransDetailServiceClient();
TransDetailResp transDetailResp;
List<TransDetailImport> transDetailImportList = new ArrayList<>();
//组织好一个list数据:读取从start -> start+size行的数据
for (int i = start; i < start + size && i < contentList.size(); i++) {
TransDetailImport transDetailImport = new TransDetailImport();
String[] detailRow = contentList.get(i).split("\\|@\\|");
if (detailRow != null || !detailRow.equals("")) {
try {
transDetailImport.setMerchantId(data.getMerchantId());
transDetailImport.setMerchantName(data.getMerchantName());
transDetailImport.setMerchantBatchNo(data.getSettleOrderNo()); //商户批次号
transDetailImport.setMerchantBatchSerialNo(XssfCellValueCheckHelper.getStringNotEmpty(detailRow[0], i, 0)); //商户批次序号<来源:页面导入模板中明细行的序号>模板必填
transDetailImport.setMerchantOrderNo(XssfCellValueCheckHelper.getStringNotEmpty(detailRow[1], i, 1)); //商户订单号:模板必填
transDetailImport.setPlatformOrderNo(XssfCellValueCheckHelper.getRealOrDefaultValue(detailRow[2], detailRow[1])); //平台订单号(支付退款订单号):如果不送默认商户订单号
transDetailImport.setMerchantTransDate(detailRow[4].equals("") ? new Date() : new Date(detailRow[4])); //商户交易日期:如果不送默认上送日期
transDetailImport.setTransType(XssfCellValueCheckHelper.getRealOrDefaultValue(detailRow[5], TransTypeEnums.Payment.getValue())); //交易类型:如果不送默认支付
transDetailImport.setOriginOrderNo(XssfCellValueCheckHelper.checkAndGetOriginOrderNo(detailRow[3], transDetailImport.getTransType(), i)); //原支付订单号
transDetailImport.setCurrency(XssfCellValueCheckHelper.getRealOrDefaultValue(detailRow[6], "CNY")); //币种:三位货币代码,如果不送默认CNY:人民币
transDetailImport.setAmount(XssfCellValueCheckHelper.getAmount(detailRow[7], i)); //交易金额:外部交易上传金额,内部商户订单金额
transDetailImport.setCustomerName(XssfCellValueCheckHelper.getStringNotEmpty(detailRow[9], i, 9)); //客户名称:模板必填