超大数据量的xlsx格式的excel文件的读取和解析,解决了POI方式的内存溢出和性能问题

在之前的博文《 POI读取并解析xlsx格式的excel文件》中,在小数据量的情况下是可以轻松愉快的处理的,但是当excel文件的数据量达到百万级的时候,
  1. InputStream is = files[i].getInputStream();  
  2. 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));        //客户名称:模板必填
  • 5
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 21
    评论
你可以使用Apache POI库来解析Excel文件。下面是一个使用Java解析Excel文件的示例代码: ```java import java.io.File; import java.io.FileInputStream; import java.io.IOException; import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.hssf.usermodel.HSSFWorkbook; public class ExcelParser { public static void main(String[] args) { String filePath = "path/to/your/excel/file.xlsx"; // 替换为你的Excel文件路径 try { FileInputStream fis = new FileInputStream(new File(filePath)); Workbook workbook; if (filePath.endsWith(".xlsx")) { workbook = new XSSFWorkbook(fis); // 处理.xlsx文件 } else if (filePath.endsWith(".xls")) { workbook = new HSSFWorkbook(fis); // 处理.xls文件 } else { throw new IllegalArgumentException("The specified file is not Excel file"); } Sheet sheet = workbook.getSheetAt(0); // 获取第一个工作表 for (Row row : sheet) { for (Cell cell : row) { CellType cellType = cell.getCellType(); if (cellType == CellType.STRING) { System.out.print(cell.getStringCellValue() + " "); } else if (cellType == CellType.NUMERIC) { System.out.print(cell.getNumericCellValue() + " "); } else if (cellType == CellType.BOOLEAN) { System.out.print(cell.getBooleanCellValue() + " "); } } System.out.println(); } workbook.close(); fis.close(); } catch (IOException e) { e.printStackTrace(); } } } ``` 请将代码中的`"path/to/your/excel/file.xlsx"`替换为你实际的Excel文件路径。该代码会打开Excel文件并输出每个单元格的值。你可以根据需要对解析的内容进行进一步处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值