功能概述
主要实现的功能:
1.分页查询,避免一次性查询全部数据加载到内存引起频繁FULL GC甚至OOM
2.当数据量超过单个工作簿最大行数(1048575)时,自动将数据写入新的工作簿
3.支持百万级数据量导出
具体实现
第一,定义数据实体父类和分页对象。
// 数据实体父类 public abstract class DataEntity {} // 分页对象 @Data public class Page<T> { /** 分页参数,第几页 */ protected int pageNo; /** 分页参数,当前页数量 */ protected int pageSize; /** 数据列表 */ protected List<T> list; /** 数据总量 */ protected long count; public Page(int pageNo, int pageSize) { this.pageNo = pageNo; this.pageSize = pageSize; } }
第二,既然是分页查询导出,就需要一个实现分页查询的接口。
// 数据分页查询接口 public interface PageQuerier<T> { /** * 查询指定页数据,同时会查询总数 * * @param page * @param entity * @return */ Page<T> findPage(Page<T> page, DataEntity entity); /** * 查询指定页数据,只查询数据,不查询总数 * * @param page * @param entity * @return */ List<T> findList(Page<T> page, DataEntity entity); }
第三,封装分页查询并导出数据的核心服务类。
// 分页导出Excel public class ExcelExporter<T> { /** 实现分页查询 */ private PageQuerier<T> pageQuerier; /** * @param pageQuerier 分页查询服务 */ public ExcelExporter(PageQuerier<T> pageQuerier) { this.pageQuerier = pageQuerier; } /** * 分页查询数据并写入到下载输出流 * * @param response * @param fileName 文件名,如:测试.xlsx * @param header 表头映射类 * @param entity 数据实体对象 * @throws IOException */ public void write(HttpServletResponse response, String fileName, Class header, DataEntity entity) throws IOException { if (header == null) { header = entity.getClass(); } // 内容样式策略 WriteCellStyle contentWriteCellStyle = new WriteCellStyle(); // 垂直居中,水平居中 contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER); contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); contentWriteCellStyle.setBorderLeft(BorderStyle.THIN); contentWriteCellStyle.setBorderTop(BorderStyle.THIN); contentWriteCellStyle.setBorderRight(BorderStyle.THIN); contentWriteCellStyle.setBorderBottom(BorderStyle.THIN); HorizontalCellStyleStrategy contentStyleStrategy = new HorizontalCellStyleStrategy(); contentStyleStrategy.setContentWriteCellStyleList(Arrays.asList(new WriteCellStyle[]{contentWriteCellStyle})); // 一定要对文件名做URL编码,否则会出现中文乱码 String file = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()); response.reset(); response.addHeader("Content-Disposition", "attachment;filename=" + file); response.addHeader("filename", file); response.setContentType("application/octet-stream; charset=UTF-8"); ExcelWriter writer = new ExcelWriterBuilder() .autoCloseStream(true) .automaticMergeHead(false) .excelType(ExcelTypeEnum.XLSX) .file(response.getOutputStream()) .head(header) .registerWriteHandler(contentStyleStrategy) .build(); // xlsx文件单个工作簿上限是1048575行,这里如果超过104W需要分Sheet long sheetMax = 1048575; // 每一个工作簿可写入的数据量,需要把表头所占的行算上 long sheetTotal = 1; // 工作簿编号 int sheetNum = 1; // 分页数 int pageNo = 1; // 每页查询数量 int pageSize = 50000; WriteSheet writeSheet = EasyExcel.writerSheet("sheet"+sheetNum).head(header).build(); // 先查询第一页,得到总数 Page<T> page = this.pageQuerier.findPage(new Page(pageNo, pageSize), entity); writer.write(page.getList(), writeSheet); if (page.getCount() <= 0 || page.getCount() <= pageSize) { // 没有数据或只有一页数据 writer.finish(); return; } sheetTotal += page.getList().size(); long total = page.getCount(); long fetch = page.getList().size(); for (;;) { // 只查询分页数据,不查询分页总数 pageNo++; List<T> list = this.pageQuerier.findList(new Page(pageNo, pageSize), entity); fetch += list.size(); sheetTotal += list.size(); if (sheetTotal >= sheetMax) { sheetTotal = 1 + list.size(); sheetNum++; writeSheet = EasyExcel.writerSheet("sheet"+sheetNum).head(header).build(); } writer.write(list, writeSheet); if (fetch >= total) { writer.finish(); break; } } writer.finish(); } }
完成上述核心接口和类的定义之后,就可以调用封装好的分页查询和导出服务了。
// 以分页查询数据库方式导出百万级数据 @RequestMapping("/downloadPage") public String downloadByPage(HttpServletRequest request, HttpServletResponse response, @RequestParam("name") String name, @RequestParam("age") int age) throws IOException { SubjectExport param = SubjectExport.builder().name(name).age(age).build(); String fileName = String.format("%s%s.xlsx", "数据导出", System.currentTimeMillis()); new ExcelExporter<SubjectExport>(this.subjectPageQuerier).write(response, fileName, SubjectExport.class, param); return null; }
完整示例代码详见:test-web-downloadtip,可以直接下载到本地运行。