功能概述
主要实现的功能: 1.分页查询,避免一次性查询全部数据加载到内存引起频繁FULL GC甚至OOM 2.当数据量超过单个工作簿最大行数(1048575)时,自动将数据写入新的工作簿 3.支持百万级数据量导出
具体实现
第一,定义数据实体父类和分页对象。
代码语言:javascript复制// 数据实体父类
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;
}
}
第二,既然是分页查询导出,就需要一个实现分页查询的接口。
代码语言:javascript复制// 数据分页查询接口
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);
}
第三,封装分页查询并导出数据的核心服务类。
代码语言:javascript复制// 分页导出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();
}
}
完成上述核心接口和类的定义之后,就可以调用封装好的分页查询和导出服务了。
代码语言:javascript复制// 以分页查询数据库方式导出百万级数据
@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,可以直接下载到本地运行。