采用Mybatis持久化框架,查询数据采用List模式返回结果,这种数据返回模式,能满足大多数的在少量返回结果集,或分页结果集的应用场景。
针对要大量返回结果集,特别是需要把超大查询结果集返回或导出的时候,List模式由于需要在应用服务器中构造对象。这不但需要消耗大量内存,而且会引起频繁的垃圾回收,造成性能降低。甚至因为内存无法容纳过大的结果集List,造成OOM异常。
有人采用分页模式,逐页导出的方式来解决。这种解决方式,针对超大结果集,比如数百万数据行时,由于可能会产生上万个分页查询,造成数据库上万次查询,给数据库性能带来灾难性影响。
本文档采用Mybatis的ResultHandler接口,采用ResultHandler回调模式,实现逐条处理,既实现一次查询获得所有数据,不引起数据库性能灾难,又避免应用服务器内存过大消耗的问题。
下面直接上实现代码。
1.依赖的包引入
<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>4.0.1</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.0.1</version> </dependency>
2.超大数据集导出EXCEL工具类ExcelGenerator,本类实现了ResultHandler,通过回调,逐条导出。具体代码
import java.io.IOException; import java.text.DateFormat; import java.text.DecimalFormat; import java.util.Map; import org.apache.ibatis.session.ResultContext; import org.apache.ibatis.session.ResultHandler; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.streaming.SXSSFSheet; import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; /** * Mybatis查询数据采用List的方式返回结果集,当结果集过大,内存无法将结果集对象全部容纳,抛出OOM异常, * 本类主要采用Mybatis提供的ResultHandler接口逐条处理数据的方式读取数据,对数据进行转化。 * 并将结果集逐行转化为EXCEL,当结果集过大,Workbook对象也会太大,故可以设置isLargeExcel=true, * 采用POI缓存机制,达到一定条目,缓存到临时文件的方式,用以实现巨量数据导出为EXCEL。 * @author tbr * * @param <T> 转化后,用于输出到EXCEL的对象类 * @param <E> Mybatis对象类 */ public abstract class ExcelGenerator<T,E> implements ResultHandler<E> { protected String[] titles; protected String[] fields; protected int colcnt; protected Map<String,DecimalFormat> numberFormats; protected Map<String,DateFormat> dateFormats; protected Workbook workbook; protected String sheetName; protected int sheetIndex; protected Sheet sheet; protected int rownum = 0; protected boolean isLargeExcel=false; /** * * @param titles excel表头名列表 * @param fields 需要输出的excel列对应的数据对象属性名列表 * @param numberFormats 需要格式化的数字类属性的输出格式映射表 * @param dateFormats 需要格式化的日期型字段属性的输出格式映射表 */ public ExcelGenerator(String[] titles, String[] fields, Map<String,DecimalFormat> numberFormats, Map<String,DateFormat> dateFormats) { if (titles==null) this.titles=fields; else this.titles=titles; this.fields=fields; this.colcnt=fields.length; this.numberFormats=numberFormats; this.dateFormats=dateFormats; } public void initWorkbook(String sheetName,boolean isLargeExcel) throws IOException{ this.isLargeExcel=isLargeExcel; this.sheetName=sheetName; if (isLargeExcel){ SXSSFWorkbook wb = new SXSSFWorkbook(-1); wb.setCompressTempFiles(true); workbook=wb; }else{ workbook = new XSSFWorkbook(); } sheetIndex=1; sheet = workbook.createSheet(sheetName+"-"+sheetIndex); createHeader(); } public Workbook getWorkbook(){ return workbook; } @Override public void handleResult(ResultContext<? extends E> resultContext) { rownum++; try { E entity=resultContext.getResultObject(); if (entity==null){ return; } createRow(convert(entity)); } catch (IOException e) { e.printStackTrace(); } } /** * 数据转化处理 */ public abstract T convert(E entity); private void createRow(Object data) throws IOException{ Row row = sheet.createRow(rownum); for(int cellnum = 0; cellnum < colcnt; cellnum++){ createCell(rownum,row,cellnum,data); } if (isLargeExcel && rownum % 1000 == 0) { // 每100行,缓存一下,减少内存消耗 //System.out.println("rownum:"+rownum+"-----------"+System.currentTimeMillis()); ((SXSSFSheet)sheet).flushRows(1000); } if (rownum % 1000000 == 0) { //一百万行,另外创建一个sheet sheetIndex++; String newSheetName=sheetName+"-"+sheetIndex; //System.out.println("rownum:"+rownum+"-----------"+newSheetName); sheet=workbook.createSheet(newSheetName); createHeader(); } } private void createCell(int rownum,Row row,int cellnum,Object data) throws IOException{ Cell cell = row.createCell(cellnum); String fieldName=fields[cellnum]; Object fieldValue=getFieldData(data,fieldName); Object cellValue=fieldValue; if ( numberFormats != null && numberFormats.containsKey(fieldName) ){ cellValue=numberFormats.get(fieldName).format(fieldValue); } if ( dateFormats != null && dateFormats.containsKey(fieldName) ){ cellValue=dateFormats.get(fieldName).format(fieldValue); } if (cellValue!=null){ cell.setCellValue(cellValue.toString()); }else{ cell.setCellValue(""); } } private Object getFieldData(Object data,String fieldName){ Object ret=data; String[] fieldList=fieldName.split("\\."); for(String fn:fieldList){ if (ret==null) return ret; try { ret=BeanUtils.forceGetProperty(ret,fn); } catch (NoSuchFieldException e) { e.printStackTrace(); ret=null; } } return ret; } private void createHeader(){ rownum=0; //输出表头 Row row = sheet.createRow(rownum); Cell cell; for (int cellnum=0;cellnum<titles.length;cellnum++ ){ cell = row.createCell(cellnum); cell.setCellValue(titles[cellnum]); } } }
3.使用方法
定义使用ResultHandler的Mapper方法。
import org.apache.ibatis.annotations.*; import org.apache.ibatis.mapping.ResultSetType; import org.apache.ibatis.session.ResultHandler; @Mapper public interface XXXXMapper { ...... @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000) @ResultType(XXXX.class) void query1By1(@Param("params") Map params,ResultHandler<XXXX> handler); ...... }
4.导出数据例子
....... @RequestMapping("2excel") public void test2Excel() throws IOException{ String fileName="测试文件"; String[] titles={......};//excel标题列表 String[] fields={......};//导出到excel的属性列表 Map<String,DecimalFormat> numberFormats=null;//数字格式map Map<String,DateFormat> dateFormats=null;//日期格式map String fn=java.net.URLEncoder.encode(fileName, "UTF-8"); response.setCharacterEncoding("utf-8"); OutputStream out; response.setContentType("Content-Type: application/vnd.ms-excel"); response.setHeader("Content-disposition", "attachment; filename=" + fn + ".xlsx"); out = response.getOutputStream(); Map<String,Object> paras=new HashMap<>(); ExcelGenerator<BizBean,EOBean> deu=new ExcelGenerator<BizBean,EOBean>(titles,fields,numberFormats,dateFormats){ public BizBean convert(EOBean entity){ BizBean bizentity=new BizBean(); .......//对象转化,将数据库实体对象转化为导出所需要的业务对象 return bizentity; } }; deu.initWorkbook(fileName, true); appInfoMapper.query1By1(paras,deu); Workbook wb=deu.getWorkbook(); wb.write(out); out.flush(); if (wb instanceof SXSSFWorkbook){ ((SXSSFWorkbook)wb).dispose(); } wb.close(); out.close(); return; } ......
到此,你就可以照此开发超大规模数据的导出了。你不但可以导出几百几千的,你甚至可以导出上亿的。只是这个EXCEL太大了,你的OFFICE是否能打开就不知道。
5.写在后面
当然,你也可以修改一下,导出为CSV文件,这个就没有任何数据量的限制了。
是不是很爽