需求:需要从数据库查询出几十上百万的数据并导出成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") }