最近遇到个用户数据批量导出excel的需求,第一次看到这个需求大家第一时间想到的应该大多都是easy-excel
这个框架吧,哈哈,我第一时间想到的也是这个框架。
但是对于少量的数据,比如有10个用户这样的数据肯定没有啥太大的问题,但是对于百万级数据可能就会有问题,总不能一次性吧100w数据从数据库里面查出来吧,这无疑是sql炼狱,同时也面临着oom的风险,面对一个线上服务这显然是不理智的。
面对如上情况大多数情况的解决思路都是分批量查询数据,比如100w数据每次查询10w或者1w分多次查询出来,以时间换空间。面对如此情况easy-excel版本可能支持的不是很好,那么下面就提供一下该场景的解决方案。
实际在解决该需求时还要考虑到excel的最大可承载数据量为 1048576
行(该数据由百度查询得到,也可自行下拉excel表格到最底端查看),但是如果数据超过这些行数怎么办,直接报错?显然这对于一个功能来说是不可取的,这时可以设定一个边界值来指定超过该边界值后导出多个excel,但是把多个excel直接导出到服务器上,把地址返回给前端,让前端拿着地址去web服务器上面下载,也不是一个可取的方法。但是如何能让多个excel作为一个文件导出呢?方案到了这大家可能都会想到了,那就是.zip
这种压缩文件的形式。现在有了思路了,那么说干就干,下面我们直接来看一下代码。
/**
* 导出excel工具
* @author wangshaoyu
* @date 2022/11/20
*/
public class ExcelUtils {
/**
* 导出多个sheet到多个excel文件,并压缩到一个zip文件
* @param zipFilename 下载时压缩包名称
* @param response 请求返回流
* @param header excel实体类类型
* @param splitTimes 次数分界,excel数据导入多少次后创建下一个excel文件
* @param supplier 供给侧函数式接口,提供get方法,该方法可自定义
* @return void
*/
public static <T> void exportZip(String zipFilename, HttpServletResponse response, Class<T> header, int splitTimes, Supplier<List<T>> supplier) {
if (zipFilename == null || zipFilename.isEmpty()) {
zipFilename = "export";
} else if (zipFilename.toLowerCase(Locale.ROOT).endsWith(".zip")) {
zipFilename = zipFilename.substring(0, zipFilename.length() - 3);
}
try {
// 这里URLEncoder.encode可以防止中文乱码
String downFileName = URLEncoder.encode(zipFilename, "utf-8");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + downFileName + ".zip");
response.setContentType("application/x-msdownload");
response.setCharacterEncoding("utf-8");
//开始存入
try (ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream())) {
List<T> exportData = null;
int count = 0;
int fileIndex = 1;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ExcelWriterBuilder builder = EasyExcel.write(outputStream).autoCloseStream(false)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy());
ExcelWriter excelWriter = builder.build();
zipOut.putNextEntry(new ZipEntry(String.format("%s-%d.xls", zipFilename, fileIndex)));
WriteSheet writeSheet = EasyExcel.writerSheet(zipFilename).head(header).build();
exportData = supplier.get();
if (Func.isEmpty(exportData)) {
throw new ServiceException("导出数据为空");
}
while (Func.isNotEmpty(exportData)) {
count += 1;
excelWriter.write(exportData, writeSheet);
exportData = supplier.get();
//如果到了分割点,则证明该excel文件已经到达存储临界值,直接保存该文件
if (count % splitTimes == 0) {
fileIndex += 1;
excelWriter.finish();
outputStream.writeTo(zipOut);
zipOut.closeEntry();
//如果导出数据不为空则证明存在下一个excel文件,则在压缩包中新增一个excel文件
if (Func.isNotEmpty(exportData)) {
outputStream = new ByteArrayOutputStream();
builder = EasyExcel.write(outputStream).autoCloseStream(false)
// 自动适配
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy());
excelWriter = builder.build();
zipOut.putNextEntry(new ZipEntry(String.format("%s-%d.xls", zipFilename, fileIndex)));
writeSheet = EasyExcel.writerSheet(zipFilename).head(header).build();
}
}
}
//count % splitTimes == 0 代表本次导出刚好导出整数个文件,且每个文件中都有规定大小的数据
if (count % splitTimes == 0) {
return;
}
excelWriter.finish();
outputStream.writeTo(zipOut);
zipOut.closeEntry();
} catch (IOException e) {
throw new RuntimeException("导出Excel异常", e);
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("导出excel异常");
}
}
/**
* 导出单个excel文件,适用于大量数据但是未超过excel最大限制的情况
* @param fileName 文件名称
* @param response 请求返回数据流
* @param header excel实体类类型
* @param supplier 供给侧函数
* @return void
* */
public static <T> void exportExcel(String fileName, HttpServletResponse response, Class<T> header, Supplier<List<T>> supplier) {
try {
if (fileName == null || fileName.isEmpty()) {
fileName = "export";
} else if (fileName.toLowerCase(Locale.ROOT).endsWith(".xls")) {
fileName = fileName.substring(0, fileName.length() - 3);
} else if (fileName.toLowerCase(Locale.ROOT).endsWith(".xlsx")) {
fileName = fileName.substring(0, fileName.length() - 4);
}
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
String excelName = URLEncoder.encode(fileName, "utf-8");
response.setHeader("Content-disposition", "attachment;filename=" + excelName + ".xls");
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).autoCloseStream(false)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();
WriteSheet writeSheet = EasyExcel.writerSheet(fileName).head(header).build();
List<T> exportData = null;
while (Func.isNotEmpty(exportData = supplier.get())) {
excelWriter.write(exportData, writeSheet);
}
excelWriter.finish();
} catch (Exception e) {
throw new ServiceException("导出excel异常");
}
}
}
对于工具类使用、代码bug、优化问题欢迎评论提出,作者会努力优化,耐心回答。