大批量导出excel逻辑(支持异步和同步配置)

需求:需要从数据库查询出几十上百万的数据并导出成excel文件

问题:

1、传统导出方式在几万条数据时还可以胜任,但是数量一旦有几十万甚至上百万的话就会出现内存不够用,内存溢出等问题。

2、大批量导出一般会和业务结合比较紧密,如何抽象出通用工具类

技术关键点:

1、解决大批量导出内存消耗问题

2、通用类的抽象

实现思路:

1、因为大部分内存问题发生在查询数据库和生成excel两个地方,所以针对这两个地方进行改造,

第一点,查询时使用分页方式进行查询

第二点,每生成1W条数据时生成一个excel文件到硬盘中,最后将多个excel打包成一个zip文件并导出。同时在使用POI插件进行处理时进行刷盘控制(详细看后边的代码示例,或者参考POI官方文档)

2、抽象工具类中需要实现业务上的分页查询逻辑,然后还要实现异步导出逻辑,可以考虑使用抽象类实现,在关键业务对象上交给子类实现(模板模式)

核心代码(注意代码可能不全,主要是核心的代码类和方法,辅助的代码类比如http上传之类的没有加入,自己实现就好了)

 

public abstract class BaseExportService {
    private static final String EVENT_NAME = "大批量导出抽象服务";
    private static final int ACT_GOODS_EXPORT_LIMIT_DEFAULT = 100;
    private static final int IS_SYNC_EXPORT_MAX_NUM_DEFAULT = 100;
    private static final int ACT_GOODS_EXPORT_DIVIDE_PAGE_DEFAULT = 100;
    private static final int ACT_GOODS_EXPORT_LIMIT_EXCEL_DEFAULT = 100;

    /**
     * 描述: 导出方法
* 作者:jeff.meng
* 版本:V1.0
* * @param params * @return O */ public O export(I params) throws Exception { // 获取活动id对应活动信息的map TI query = getQuery(params); // 查询总条数 int count = getExportExcelCount(params, query); if (count == 0) { throw new ExportBatchException(GlobalErrorCode.DATA_NULL_ERROR); } // 查询配置的导出上限 int exportDataLimit = getExportDataLimit(); if (count > exportDataLimit) { throw new ExportBatchException(GlobalErrorCode.DATA_MAX_LIMIT_ERROR); } // 导出逻辑 FileFidModel fileFidModel = coreExportHandle(params, count, query); return assembleResult(params, fileFidModel, query); } /** * 描述: 获取查询条件
* 作者:jeff.meng
* 版本:V1.0
* * @param params * @return TI */ protected abstract TI getQuery(I params); /** * 描述: 获取导出的总条数
* 作者:jeff.meng
* 版本:V1.0
* * @param params * @param query * @return int */ protected abstract int getExportExcelCount(I params, TI query); /** * 描述: 获取生成文件名称的前置名称
* 作者:jeff.meng
* 版本:V1.0
* * @param * @return java.lang.String */ protected abstract String getPrefixName(I params); /** * 描述: 获取sheet名称
* 作者:jeff.meng
* 版本:V1.0
* * @param * @return java.lang.String */ protected abstract String getSheetName(I params); /** * 描述: 写入其他信息到最终的列表中
* 作者:jeff.meng
* 版本:V1.0
* * @param selectedGoodsList * @return java.util.List */ protected abstract List assembleOtherInfos(List selectedGoodsList); /** * 描述: 分页查询数据信息
* 作者:jeff.meng
* 版本:V1.0
* * @param query * @param pager * @return java.util.List */ protected abstract List getInfoListByPager(TI query, Pager pager) throws Exception; /** * 描述: 获取导出excel的标题行
* 作者:jeff.meng
* 版本:V1.0
* * @param params * @return java.lang.String[][] */ protected abstract String[][] getExportTitle(I params); /** * 描述: 组装为返回的数据
* 作者:jeff.meng
* 版本:V1.0
* * @param params * @param fileFidModel * @param query * @return O */ protected abstract O assembleResult(I params, FileFidModel fileFidModel, TI query); private List asyncExportExcelNew(I params, TI query) throws Exception { ActExportHelper actExportHelper = new ActExportHelper(); int pageSize = getExportDividePage(); int divideExcelNum = getExportExcelDataLimit(); int totalNum = 0; // 遍历活动类型 try { String[][] sheetTitles = getExportTitle(params); String sheetName = getSheetName(params); actExportHelper.initializeHelper(sheetTitles, sheetName); String prefixName = getPrefixName(params); // 分页查询商品信息,一个活动一个活动的处理分页 for (int i = 1; i < Integer.MAX_VALUE; i++) { Pager pager = new Pager(); pager.setSize(pageSize); pager.setPage(i); List selectedGoodsList = getInfoListByPager(query, pager); // 写入其他信息 List promactGoodsDtoList = assembleOtherInfos(selectedGoodsList); if (CollectionUtils.isEmpty(promactGoodsDtoList)) { break; } // 写入内容到workbook actExportHelper.createExcelBody(promactGoodsDtoList, sheetTitles); // 记录当前workbook写的总条数 actExportHelper .setWorkBookTotalRows(actExportHelper.getWorkBookTotalRows() + promactGoodsDtoList.size()); totalNum = totalNum + promactGoodsDtoList.size(); // 如果当前workbook写的总条数没有超过拆分excel的条数,则继续 if (actExportHelper.getWorkBookTotalRows() < divideExcelNum) { continue; } else { // 超过了拆分数量则写入workbook actExportHelper.writeFile(prefixName); // 写入后要重新生成workbook,和sheet,以及标题行 actExportHelper.initializeHelper(sheetTitles, sheetName); continue; } } // 一种活动类型查询完成的时候需要写入一个文件 actExportHelper.writeFile(prefixName); } catch (ExportExcelException e) { // 删除已经生成的内存中的问题件 actExportHelper.handlerException(); throw new Exception("文件导出异常"); } // 生成完成,执行压缩并上传文件 List fidList = new ArrayList<>(); try { fidList = ExportUtil.exportDataFiles(actExportHelper.fileNameList); } catch (ExportExcelException e) { actExportHelper.handlerException(); throw new Exception("文件导出异常"); } return fidList; } /** * 描述: 导出功能的核心处理逻辑
* 作者:jeff.meng
* 版本:V1.0
* * @param params * @param count * @param query * @return com.vip.vis.act.export.model.FileFidModel */ private FileFidModel coreExportHandle(I params, int count, TI query) throws Exception { FileFidModel fileFidModel = new FileFidModel(); // 获取同步导出最大数量 int getIsSyncMaxNum = getIsSyncMaxNum(); if (count > getIsSyncMaxNum) { // 异步导出 fileFidModel.setSync(false); this.asyncExportExcel(params, count, query); } else { // 同步导出 fileFidModel.setSync(true); List fidList = asyncExportExcelNew(params, query); List fileFidList = new ArrayList<>(); for (String fid : fidList) { FileFid fileFid = new FileFid(); fileFid.setFid(fid); fileFidList.add(fileFid); } fileFidModel.setFidList(fileFidList); } return fileFidModel; } private void asyncExportExcel(I params, int count, TI query) { Callable task = new Callable() { @Override public Object call() throws Exception { try { asyncExportExcelNew(params, query); } catch (Exception e) { throw new Exception("文件导出异常"); } return "SUCCESS"; } }; // 异步执行导出 ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(1)); service.submit(task); } protected abstract int getExportDataLimit() throws Exception; protected abstract int getIsSyncMaxNum() throws Exception; protected abstract int getExportDividePage() throws Exception; protected abstract int getExportExcelDataLimit() throws Exception; }
大批量导出helper
public class ActExportHelper {
    // 导出类型对应sheet名称的map
    private static final String EVENT_NAME = "导出ExcelHelper";
    SXSSFWorkbook workbook = null;
    Sheet titleSheet = null;
    CellStyle titleStyle = null;
    CellStyle dataStyle = null;
    // 记录workbook写入的总条数
    int workBookTotalRows = 0;
    public List fileNameList = new ArrayList<>();

    /**
     * 描述:创建excel表格的标题行
* 作者:jeff.meng
* 版本:V1.0
* * @param first */ public void createExcelTitle(Sheet first, String[][] titles) { Row row = first.createRow(0); for (int i = 0; i < titles.length; i++) { Cell cell = row.createCell(i); cell.setCellType(HSSFCell.CELL_TYPE_STRING); cell.setCellValue(titles[i][0]); cell.setCellStyle(titleStyle); setColumnWidth(first, i, cell.getStringCellValue()); } } /** * 描述:创建excel表格的数据
* 作者:jeff.meng
* 版本:V1.0
* * @param list * @throws Exception */ public void createExcelBody(List list, String[][] titles) { int rowNum = this.workBookTotalRows + 1; for (T data : list) { Row rowData = titleSheet.createRow(rowNum++); // 获取data对象中属性名称与属性值映射 Map fieldNameValueMap = getFieldNameValueMap(data); if (MapUtils.isEmpty(fieldNameValueMap)) { continue; } for (int i = 0; i < titles.length; i++) { String key = titles[i][1]; Object fieldValue = fieldNameValueMap.get(key); Cell cell = rowData.createCell(i); cell.setCellType(HSSFCell.CELL_TYPE_STRING); String cellValue; if (fieldValue == null) { cellValue = ""; } else if (fieldValue instanceof Number) { cellValue = ((Number) fieldValue).toString(); } else if (fieldValue instanceof Boolean) { cellValue = ((Boolean) fieldValue).toString(); } else if (fieldValue instanceof Date) { cellValue = DateUtil.formatDatetime((Date) fieldValue); } else if (fieldValue instanceof Collection) { cellValue = StringUtil.join((Object[]) fieldValue, ","); } else { cellValue = (String) fieldValue; } cell.setCellValue(cellValue); cell.setCellStyle(dataStyle); setColumnWidth(titleSheet, i, cell.getStringCellValue()); } } } /** * 描述:通过字段名从对象或对象的父类中得到字段的值并以Map结构返回
* 作者:jeff.meng
* 版本:V1.0
* * @param object * @return java.util.Map */ public static Map getFieldNameValueMap(Object object) { Map fieldNameValueMap = new HashMap<>(); if (object == null) { return fieldNameValueMap; } try { Class clazz = object.getClass(); for (; clazz != Object.class; clazz = clazz.getSuperclass()) { // 获取本类中所有的get方法 for (Method method : clazz.getDeclaredMethods()) { String methodName = method.getName(); if (methodName.startsWith("get")) { String fieldName = StringUtil.lowerCaseFirst(methodName.substring(3)); fieldNameValueMap.put(fieldName, method.invoke(object)); } } } } catch (Exception e) { // 这里什么都不做,并且这里的异常不能抛出去 // 如果这里的异常打印或者往外抛,则就不会执行clazz = clazz.getSuperclass(),最后就不会进入到父类中了 // ActLogger.warn(EVENT_NAME, "根据字段名获取字段值:object={}", object); } return fieldNameValueMap; } /** * 描述:根据单元格值动态设置相应的列宽
* 作者:jeff.meng
* 版本:V1.0
* 说明:POI设置列宽的参数以一个字符的1/256的宽度作为一个单位
* * @param first * @param columnIndex * @param cellValue */ public static void setColumnWidth(Sheet first, int columnIndex, String cellValue) { int columnWidth = calColumnWidth(cellValue); if (columnWidth > first.getColumnWidth(columnIndex)) { first.setColumnWidth(columnIndex, columnWidth); } } /** * 描述:根据单元格值得到该列应该设置的宽度
* 作者:jeff.meng
* 版本:V1.0
* * @param cellValue * @return */ private static int calColumnWidth(String cellValue) { if (StringUtil.isEmpty(cellValue)) { return 0; } int length = cellValue.getBytes().length; if (!isChineseByREG(cellValue)) { length = length + 4; } // 由于当单元格大于255*256 会抛出异常,进行判断。 if (length > 100) { length = 100; } return length * 256; } /** * 描述:判断字符串中是否包含中文
* 作者:jeff.meng
* 版本:V1.0
* * @param str * @return */ private static boolean isChineseByREG(String str) { if (str == null) { return false; } Pattern pattern = Pattern.compile("[\\u4E00-\\u9FBF]+"); return pattern.matcher(str.trim()).find(); } /** * 描述:列头单元格样式
* 作者:jeff.meng
* 版本:V1.0
* * @param workbook * @return */ public CellStyle createTitleStyle(SXSSFWorkbook workbook) { Font font = createFont(workbook, 11, "宋体"); // 字体加粗 font.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD); // 设置样式 CellStyle style = createStyle(workbook); // 在样式用应用设置的字体 style.setFont(font); return style; } /** * 描述:列数据信息单元格样式
* 作者:jeff.meng
* 版本:V1.0
* * @param workbook * @return */ public CellStyle createDataStyle(SXSSFWorkbook workbook) { Font font = createFont(workbook, 11, "宋体"); // 设置样式 CellStyle style = createStyle(workbook); // 在样式用应用设置的字体 style.setFont(font); return style; } /** * 描述:根据是否有指定前缀拼接文件名
* 作者:jeff.meng
* 版本:V1.0
* * @param filePrefix * @return */ public static String getFileName(String filePrefix) { String fileName = ""; if (StringUtil.isBlank(filePrefix)) { fileName = EVENT_NAME + CommonUtil.createUuid() + ".xlsx"; } else { fileName = filePrefix + CommonUtil.createUuid() + ".xlsx"; } return fileName; } /** * 描述:创建样式
* 作者:jeff.meng
* 版本:V1.0
* * @param workbook * @return */ private CellStyle createStyle(SXSSFWorkbook workbook) { // 设置样式 CellStyle style = workbook.createCellStyle(); // 设置自动换行 style.setWrapText(false); // 设置水平对齐的样式为居中对齐 style.setAlignment(HSSFCellStyle.ALIGN_CENTER); // 设置垂直对齐的样式为居中对齐 style.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER); return style; } /** * 描述:创建字体
* 作者:jeff.meng
* 版本:V1.0
* * @param workbook * @param fontHeight * @param fontName * @return */ private static Font createFont(SXSSFWorkbook workbook, int fontHeight, String fontName) { // 设置字体 Font font = workbook.createFont(); // 设置字体大小 font.setFontHeightInPoints((short) fontHeight); // 设置字体名字 font.setFontName(fontName); return font; } public SXSSFWorkbook newWorkbook() { this.workbook = new SXSSFWorkbook(2000); this.dataStyle = createDataStyle(workbook); this.titleStyle = createTitleStyle(workbook); return this.workbook; } public SXSSFWorkbook getWorkbook() { return workbook; } public void setWorkbook(SXSSFWorkbook workbook) { this.workbook = workbook; } public int getWorkBookTotalRows() { return workBookTotalRows; } public void setWorkBookTotalRows(int workBookTotalRows) { this.workBookTotalRows = workBookTotalRows; } private static final String PATH = "./temp/"; /** * 描述:根据文件名和workbook
* 作者:jeff.meng
* 版本:V1.0
* * @throws ExportExcelException */ public void writeFile(String filePerfix) throws ExportExcelException { // 写文件的时候先判断内容是否为空,不为空才进行写文件操作 if (this.workBookTotalRows == 0) { this.clearHelper(); return; } String fileName = getFileName(filePerfix); final String fullPath = PATH + fileName; // 如果文件不存在则创建 createFullFile(fullPath); File exportFile = new File(fullPath); FileOutputStream out = null; try { out = new FileOutputStream(exportFile); workbook.write(out); fileNameList.add(fileName); clearHelper(); } catch (FileNotFoundException e) { throw new ExportExcelException("没有找到路径,path=" + fullPath, e); } catch (IOException e) { throw new ExportExcelException("创建文件异常,path=" + fullPath, e); } catch (Exception e) { throw new ExportExcelException("生成excel时发生异常:", e); } finally { if (out != null) { try { out.close(); } catch (IOException e) { throw new ExportExcelException("关闭输出流时发生错误:", e); } } } } /** * 描述: 生成workbook
* 作者:jeff.meng
* 版本:V1.0
* * @param sheetTitles * @return void */ public void initializeHelper(String[][] sheetTitles, String sheetName) { if (this.workbook == null) { // 还没有workbook,创建workbook newWorkbook(); titleSheet = StringUtil.isBlank(sheetName) ? workbook.createSheet() : workbook.createSheet(sheetName); createExcelTitle(titleSheet, sheetTitles); } } public void clearHelper() { this.workbook = null; this.titleSheet = null; this.workBookTotalRows = 0; } /** * 描述:创建文件
* 作者:jeff.meng
* 版本:V1.0
* * @param path * @throws IOException */ private static void createFullFile(String path) throws ExportExcelException { if (StringUtil.isEmpty(path)) { throw new ExportExcelException("创建文件传入的路径不能为空"); } try { // 获得文件对象 File file = new File(path); if (file.exists()) { return; } File parent = file.getParentFile(); // 如果路径不存在,则创建 if ((parent != null) && (!parent.exists())) { parent.mkdirs(); } file.createNewFile(); } catch (IOException e) { throw new ExportExcelException("创建文件错误,path=" + path, e); } } /** * 描述: 如果过程中发生了异常的处理
* 作者:jeff.meng
* 版本:V1.0
* * @param * @return void */ public void handlerException() { if (CollectionUtil.isNotEmpty(fileNameList)) { // 删除已经生成的文件 for (String fileName : fileNameList) { final String fullPath = PATH + fileName; File file = new File(fullPath); if (!file.delete()) { file.delete(); } } } } }

 数据模型

public class ExportExcelModel {

    /**
     * 工作表的名称
     */
    private String sheetName;

    /**
     * 表头
     */
    private String[][] titles;

    /**
     * 数据集
     */
    private List list;

    public String getSheetName() {
        return sheetName;
    }

    public void setSheetName(String sheetName) {
        this.sheetName = sheetName;
    }

    public String[][] getTitles() {
        return titles;
    }

    public void setTitles(String[][] titles) {
        this.titles = titles;
    }

    public List getList() {
        return list;
    }

    public void setList(List list) {
        this.list = list;
    }
}
public class Pager implements java.io.Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 

* 页码,从1开始,默认1 */ private Integer page = 1; /** *

* 每次获取记录数,默认20 */ private Integer size = 20; public Integer getPage() { return this.page; } public void setPage(Integer value) { this.page = value; } public Integer getSize() { return this.size; } public void setSize(Integer value) { this.size = value; } public String toString() { return "page:" + this.getPage() + "," + "size:" + this.getSize(); } }

gradle构建的jar

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
    compile("org.apache.poi:poi-ooxml:3.15")
    compile("org.apache.httpcomponents:httpmime:4.5.1")
    compile("org.slf4j:slf4j-api:1.7.15")
    compile("com.alibaba:fastjson:1.2.32")
    compile group: 'com.google.guava', name: 'guava', version: '21.0'
    compile("com.google.guava:guava:21.0")
    compile("org.apache.commons:commons-lang3:3.3.2")
}

 

 

你可能感兴趣的:(工具类)