【知識積累】報表優化 - 使用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);
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章