【知识积累】报表优化 - 使用SXSSF分批处理大数据量Excel导出

问题:报表系统导出几十万大数据量会导致系统卡死,需要进行优化
解决方案:1、异步处理  2、分批处理  3、分文件处理(暂时没做)

一、异步处理

在springboot项目中,实现异步处理特别简单,加两个注解(@EnableAsync、@Async)就完事儿了,在传统的web项目中,实现异步处理有点点复杂。

1、配置文件修改

xmlns:task="http://www.springframework.org/schema/task"
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd

2、添加注解

TIPS:调用方法和异步方法不能在同一个类。

二、分批处理

1、技术选型

开始我考虑导出CSV(Comma-Separated Values)文件,如果不要求格式、样式、公式等等,会比POI快很多,现在项目使用的是POI的XSSF,后面又了解到POI的HSSF和SXSSF。此时,我在想这三者有什么关系和区别?

HSSF:Excel97-2003版本,扩展名为.xls。一个sheet最大行数65536,最大列数256。

XSSF:Excel2007版本开始,扩展名为.xlsx。一个sheet最大行数1048576,最大列数16384。

SXSSF:是在XSSF基础上,POI3.8版本开始提供的支持低内存占用的操作方式,扩展名为.xlsx。

Excel版本兼容性是向下兼容。

重点讲一下SXSSF,因为是今天要使用的技术:

SXSSF扩展自XSSF,用于当非常大的工作表要导出且内存受限制的时候。SXSSF占用很少的内存是因为它限制只能访问滑动窗口的数据,而XSSF可以访问文档中所有的数据。那些不在滑动窗口中的数据是不能访问的,因为它们已经被写到磁盘上了。

我们可以通过SXSSFWorkbook workbook = new SXSSFWorkbook(int windowSize);设置滑动窗口大小,默认是100。如果设置为-1,则表示不限,没有记录被自动刷新到磁盘,除非你手动调用flushRow()刷新。当通过sheet.createRow();创建新行时,总的行数可能会超过窗口大小,这个时候行号最低的那行会被刷新到磁盘而且不能通过getRow()访问。

我们也可以通过sheet.setRandomAccessWindowSize(int windowSize);设置每个工作表的窗口大小。

注意事项:SXSSF会产生临时文件,必须明确清理,调用workbook.dispose();方法。

2、实现SXSSF

package com.km.util;

import com.alibaba.fastjson.JSONObject;
import com.km.entity.DataExport;
import com.km.entity.HospDrugSales;
import com.km.entity.RequestData;
import com.km.service.HandleExcelDataService;
import org.apache.log4j.Logger;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFCell;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFFont;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

public class ExportExcelUtils {

    protected static Logger logger = Logger.getLogger(ExportExcelUtils.class);
    private static ThreadLocal<Long> startTime = new ThreadLocal<>();

    public static void exportExcel(Map<String, Object> data, List<?> list, RequestData requestData){
        logger.info("导出数据开始,导出数量:" + list.size());
        startTime.set(System.currentTimeMillis());
        OutputStream out = null;
        SXSSFWorkbook workbook = null;
        try {
            ArrayList<Map<String,String>> arrayList = new ArrayList<>();
            String title = data.get("title").toString();
            String remark = data.get("remark").toString();
            if(list != null){
                for(int i = 0; i< list.size(); i++){
                    arrayList.add(BeanToMapUtil.convertBean(list.get(i)));
                }
            }
            workbook = ExportExcelUtils.exportExcel(title,remark, arrayList,data.get("heads").toString().split(","),data.get("fields").toString().split(","));
            //设置成null是为了让JVM优先选择回收
            arrayList = null;
            String path = requestData.getPath();
            path = path.substring(0, path.indexOf("km_edw")).replaceAll("\\\\", "/");
            String time = DateUtils.getSimpleDateTime().replaceAll(" ", "");
            StringBuilder filePath = new StringBuilder(path).append("excel").append("/").append(title).append(time).append(".xlsx");
            out = new FileOutputStream(filePath.toString());
            out.flush();
            workbook.write(out);
            long usedTime = (System.currentTimeMillis() - startTime.get())/1000;
            logger.info("导出数据结束, 导出时间:" + usedTime + "s");
            DataExport dataExport = requestData.getDataExport();
            dataExport.setExportAmount(String.valueOf(list.size()));
            dataExport.setExportUsedTime(String.valueOf(usedTime));
            dataExport.setExportAddress(filePath.toString());
        } catch (IOException e) {
            logger.error("导出报表异常,异常信息:" + e.getMessage());
        }finally{
            if(out!=null){
                try {
                    out.close();
                } catch (IOException e) {
                    logger.error(e.getMessage());
                }
            }
            if (null != workbook){
                //处理工作表在磁盘上产生的临时文件
                workbook.dispose();
            }
        }
    }

    /**
     * 使用SXSSFWorkbook导出excel
     * @param title
     * @param remark
     * @param arrayList
     * @param headers
     * @param fileds
     * @return
     */
    public static SXSSFWorkbook exportExcel(String title, String remark, ArrayList<Map<String, String>> arrayList, String[] headers, String[] fileds){
        SXSSFWorkbook workbook = new SXSSFWorkbook(1000);
        SXSSFSheet sheet = (SXSSFSheet) workbook.createSheet();
        workbook.setSheetName(0, title);
        XSSFCellStyle style = ExportExcelUtils.setSXSSFCellStyleAttribute(workbook);
        //添加第一行表头和备注
        SXSSFRow titleRow = (SXSSFRow) sheet.createRow((short) 0);
        SXSSFCell titleCell = (SXSSFCell) titleRow.createCell((short) 0);
        titleCell.setCellStyle(style);
        if(remark==null){
            remark="";
        }
        titleCell.setCellValue(title+remark);
        sheet.addMergedRegion(new CellRangeAddress(0,0,0,headers.length-1));
        // 创建第二行标题
        SXSSFRow headRow = (SXSSFRow) sheet.createRow((short) 1);
        for (int i = 0; i < headers.length; i++) {
            SXSSFCell cell = (SXSSFCell) headRow.createCell(i);
            cell.setCellStyle(style);
            XSSFRichTextString text = new XSSFRichTextString(headers[i]);
            cell.setCellValue(text);
        }
        SXSSFRow row;
        for (int i = 0; i < arrayList.size(); i++) {
            Map<String, String> map = arrayList.get(i);
            row = (SXSSFRow) sheet.createRow((i + 2));
            int j = 0;
            for (int m = 0; m < fileds.length; m++) {
                Object o = (map.get(fileds[m])) == null ? "" : (map.get(fileds[m]));
                if (o instanceof BigDecimal) {
                    row.createCell(j++).setCellValue((o).toString());
                } else{
                    row.createCell(j++).setCellValue(o.toString());
                }
            }
        }
        return workbook;
    }

    private static XSSFCellStyle setSXSSFCellStyleAttribute(SXSSFWorkbook workbook){
        //生成一个样式,用来设置标题样式
        XSSFCellStyle style = (XSSFCellStyle) workbook.createCellStyle();
        //设置这些样式
        style.setFillForegroundColor(HSSFColor.SKY_BLUE.index);
        style.setFillPattern(XSSFCellStyle.SOLID_FOREGROUND);
        style.setBorderBottom(XSSFCellStyle.BORDER_THIN);
        style.setBorderLeft(XSSFCellStyle.BORDER_THIN);
        style.setBorderRight(XSSFCellStyle.BORDER_THIN);
        style.setBorderTop(XSSFCellStyle.BORDER_THIN);
        style.setAlignment(XSSFCellStyle.ALIGN_CENTER);
        //生成一个字体
        XSSFFont font = (XSSFFont) workbook.createFont();
        font.setColor(HSSFColor.VIOLET.index);
        font.setBoldweight(XSSFFont.BOLDWEIGHT_BOLD);
        //把字体应用到当前的样式
        style.setFont(font);
        return style;
    }

}

3、导出记录表

DROP TABLE IF EXISTS `data_export`;
CREATE TABLE `data_export` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `user_id` varchar(10) DEFAULT NULL COMMENT '用户Id',
  `user_name` varchar(10) DEFAULT NULL COMMENT '用户名称',
  `export_module` varchar(20) DEFAULT NULL COMMENT '数据模块',
  `export_param` varchar(500) DEFAULT NULL COMMENT '数据参数',
  `export_address` varchar(100) DEFAULT NULL COMMENT '数据地址',
  `status` tinyint(3) DEFAULT '0' COMMENT '导出状态 0:未导出 1:已导出 2:导出错误',
  `export_amount` varchar(10) DEFAULT '0' COMMENT '导出数量',
  `export_used_time` varchar(20) DEFAULT NULL COMMENT '导出耗时,单位:秒',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
	`ext_fields` VARCHAR(500) COMMENT '扩展字段',
  PRIMARY KEY (`id`),
  KEY `user_id`(`user_id`),
  KEY `status`(`status`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='报表数据记录表';

4、下载页面

三、遇到问题

1、java获取项目的几种方法

地址1:/project_name
地址2:/home/apache-tomcat-7.0.77/project_name
地址3:/home/apache-tomcat-7.0.77/webapps/project_name/

我将文件放在了webapps的excel目录:

2、文件下载

2.1、HTML

<a  class=" btn black btn-purple" href="javascript:download('${data.id}','${data.exportAddress }');">下载</a>

2.2、JS组装Form表单

function download(id, path){
            var form = $("<form></form>");
			form.attr("action", "download.html");
			form.attr("method", "post");
			var idInput = $("<input type='text' name='id'/>");
			var pathInput = $("<input type='text' name='path' />");
            idInput.attr("value", id);
			pathInput.attr("value", path);
			form.append(pathInput);
			form.append(idInput);
			form.appendTo("body");
			form.hide();
			form.submit();
        }

2.3、后台代码

/**
     * 下载
     * @param id
     * @param path
     * @param response
     */
    @RequestMapping("/download")
    public void download(String id, String path, HttpServletResponse response){
        DataExport dataExport = new DataExport();
        File file = new File(path);
        dataExport.setStatus(Byte.valueOf("1"));
        if (file.exists()){
            try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
                 OutputStream outputStream = new BufferedOutputStream(response.getOutputStream())){
                String fileName = file.getName();
                byte[] buffer = new byte[inputStream.available()];
                inputStream.read(buffer);
                response.reset();
                response.addHeader("Content-Disposition", "attachment; filename=" + new String(fileName.getBytes("utf-8"), "iso-8859-1"));
                response.addHeader("Content-Length", String.valueOf(file.length()));
                response.setContentType("application/octet-stream");
                outputStream.write(buffer);
            } catch (FileNotFoundException e) {
                logger.error("FileNotFoundException, 文件路径:" + path + ", exception message: " + e.getMessage());
                dataExport.setStatus(Byte.valueOf("2"));
            } catch (IOException e) {
                logger.error("IOException, 文件路径:" + path + ", exception message: " + e.getMessage());
                dataExport.setStatus(Byte.valueOf("2"));
            }
        }
        dataExport.setId(Long.valueOf(id));
        reportDownloadService.updateDownloadStatus(dataExport);
    }

猜你喜欢

转载自blog.csdn.net/axin1240101543/article/details/103181925