框架描述
使用Java代码封装的一个的Excel多线程批量导出框架,使用EasyExcel完成excel文件方面的交互,重在整合功能,仅需少量代码就可以在自己的项目中快速引入该功能。借助EasyExcel支持的功能,框架可以分批流式处理写入数据到磁盘,导出文件存放在服务器一个临时路径,下载后删除,避免OOM;并且使用多线程提高数据处理速度,异步线程处理便于前端查看导出进度与处理结果。
框架代码采取了设计模式中的策略模式,只需要实现其中的策略类DataGetter
(分页获取数据),就可以快速使用导出功能,并且给出了前端的配套代码示例(vue)。
demo项目地址:https://github.com/destinyol/excel-multi-export-progress
(demo项目包括数据库sql文件及假数据生成SpringbootTest
,以及全套测试代码,可以直接部署运行)
框架文件结构:(ExcelStyle
文件可更改表格样式)

效果展示

可调参数
ExcelExportMainTool
类中有一些可以根据实际情况自行调整的参数
1 2 3 4 5 6 7 8 9 10 11
|
public static final int BATCH_COUNT = 5000;
public static final int BATCH_COUNT_QUERY = 1000;
public static final int SHEET_CUNT_NUM = 100000;
public static final String FILE_SAVE_PATH = ensureEndsWithFileSeparator(System.getProperty("java.io.tmpdir"));
public static final Boolean DEBUG_LOG_RUNNING_TIMES = false;
|
在执行过程中:
-
如果配置的每批插入Excel中的数据量BATCH_COUNT
大于分页查询的一页的数据量BATCH_COUNT_QUERY
,就会自动多线程分页查询,直到积累到BATCH_COUNT
的量才会一次性批量插入Excel文件
-
如果BATCH_COUNT
< BATCH_COUNT_QUERY
,就会一次性查询分页数据,然后循环EasyExcel写入文件。因为EasyExcel不支持多线程写入文件,所以就算放到多线程中,也要额外加锁,增加任务执行的开销,没必要。
性能测试
测试导出15万行数据,用时3.2s,文件大小7.8mb


测试导出100万行数据,用时18s,文件大小52mb


可以看到数据按照配置文件中的每10万行分了一个sheet,一共10个

1 2 3 4 5 6
| select tt.serial_number,tt.user_name,tt.depart_name,tt.project_name, tt.project_code,tt.remarks,tt.occur_time,tt.amount from td_travel tt where tt.id > #{offset} limit #{pageSize}
|
测试导出是本地自己的笔记本跑的服务,数据库是mysql,这个应该是测试的比较理想的情况,用了自增的主键id索引来分页,并且是单表查询,如果简单的用 limit #{offset},#{pageSize}
来分页,数据量大了之后深分页速度可想而知,但这是sql优化方面了,和本文关系不大。结论是实际用时和实现的分页获取处理数据的DataGetter
关系较大,也和磁盘性能cpu性能有关。
参数配置中BATCH_COUNT_QUERY
不建议太小,查询分的太散,整体性能就会降低,当然数据量小的话影响不大,所以还是要根据实际业务场景去考虑分析,在进度条刷新的粒度和整体性能之间权衡一下。
使用方法
1.创建数据实体类
首先创建一个EasyExcel的导出数据实体类,参考他的官网案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Getter @Setter @EqualsAndHashCode public class DemoData { @ColumnWidth(15) @ExcelProperty("编号") private String serialNumber;
@ColumnWidth(8) @ExcelProperty("报销人") private String userName;
@ColumnWidth(10) @ExcelProperty("部门") private String departName;
@ExcelIgnore private String ignore; }
|
2.实现DataGetter接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
public interface DataGetter {
public List<Object> readData(Integer pageSize,Integer pageNum, Object sqlFilterClass);
public Long countDataTotal(Object sqlFilterClass);
}
|
3.直接使用
1 2 3 4 5 6 7 8
|
TravelDataGetter dataGetter = new TravelDataGetter(travelMapper);
ExportProgress progress = ExcelExportMainTool .build(TravelExpenseExtraInfo.class, dataGetter, redisTemplate) .setSheetName("费用统计") .runAsync(travelSearchDto);
|
1 2 3
|
ExportProgress progress = ExcelExportMainTool.getProgress(redisTemplate, processKey);
|
1 2 3 4 5
|
ExcelExportMainTool.downloadExcel(httpResponse,fileName);
|
请求接口示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| @Autowired private RedisTemplate redisTemplate; @Autowired private TravelMapper travelMapper;
@PostMapping("exportExpenseExtraInfo") public Response startExportTravelExpenseExtraInfo(@RequestBody TravelSearchDto travelSearchDto){ Response response = new Response(); response.ret(0,"成功了"); try { TravelDataGetter dataGetter = new TravelDataGetter(travelMapper); ExportProgress res = ExcelExportMainTool.build(TravelExpenseExtraInfo.class, dataGetter, redisTemplate).setSheetName("费用统计").runAsync(travelSearchDto); response.setData(res); } catch (Exception e) { e.printStackTrace(); response.ret(111000,"出错了"); } return response; }
@GetMapping("getExportExpenseExtraInfoProgress") public Response getExportTravelExpenseExtraInfoProgress(String processKey){ Response response = new Response(); response.ret(0,"成功了"); try { ExportProgress progress = ExcelExportMainTool.getProgress(redisTemplate, processKey); response.setData(progress); } catch (Exception e) { e.printStackTrace(); response.ret(111000,"出错了"); } return response; }
@GetMapping("downloadExportExcel") public Response downloadExportTravelExpenseExtraInfoExcel(HttpServletResponse httpResponse, String fileName){ Response response = new Response(); response.ret(0,"成功了"); try { ExcelExportMainTool.downloadExcel(httpResponse,fileName); } catch (Exception e) { e.printStackTrace(); response.ret(111000,"出错了"); } return response; }
|
API文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
public ExportProgress runAsync();
public ExportProgress runAsync(Object sqlFilterClass);
public void runSync(HttpServletResponse response);
public void runSync(HttpServletResponse response,Object sqlFilterClass);
public static ExportProgress getProgress(RedisTemplate redisTemplate, String processKey);
public static void downloadExcel(HttpServletResponse response, String fileName);
public ExcelExportMainTool setSheetName(String sheetName);
|