产品经理须要导出一个页面的全部的信息到 EXCEL 文件。html
对于 excel 导出,是一个很常见的需求。前端
最多见的解决方案就是使用 poi 直接同步导出一个 excel 文件。java
若是导出的文件比较大,好比几十万条数据,同步导出页面就会卡主,用户没法进行其余操做。git
导出的时候,任务比较耗时就会阻塞主线程。github
若是导出的服务是暴露给外部(先后端分离),这种大量的数据传输十分消耗性能。数据库
使用异常处理导出请求,后台 MQ 通知本身进行处理。apache
MQ 消费以后,多线程处理 excel 文件导出,生成文件后上传到 FTP 等文件服务器。编程
前端直接查询而且展示对应的任务执行列表,去 FTP 等文件服务器下载文件便可。后端
正常的 poi 在处理比较大的 excel 的时候,会出现内存溢出。服务器
网上的解决方案也比较多。
好比官方的 SXSSF (Since POI 3.8 beta3) 解决方式。
或者使用封装好的包
原理都是强制使用 xssf 版本的Excel。
你也可使用 easyexcel,固然这个注释文档有些欠缺,并且设计的比较复杂,不是很推荐。
我这里使用的是 hutool BigExcelWriter,
懒得本身再写一遍。
若是一次查询 100W 条数据库,而后把这些信息所有加载到内存中,是不可取的。
建议有2个:
限制每一次分页的数量。好比一次最多查询 1w 条。分红 100 次查询。(必须)
虽然使用者提出要导出相似于 3 个月的全部信息,可是数量太多,毫无心义。(提出者本身可能体会不到)
尽可能避免 FULL-GC 的状况发生,由于目前的全部方式对于 excel 的输出流都会占用内存,100W 条很容易致使 FULL-GC。
去数据库读取的时候必定要记得分页,省得给数据库太大的压力。
一次读取太多,也会致使内存直线上升。
好比 100W 条数据,则分红 100 次去数据库读取。
传统的 excel 导出,都是前端一个请求,直接 HTTP 同步返回。导出 100W 条,就在那里傻等。
这客户体验不友好,并且网络传输,系统占用多种问题。
建议使用异步处理的方式,将文件上传到文件服务器。前端直接去文件服务器读取。
对于上面提到的工具,好比 Hutool,在表头的处理方面无法很方便的统一。
你能够本身定义相似于 easypoi/easyexcel 中的注解,本身反射解析。
而后统一处理表头便可。
OO 的方式操做 excel,编程更加方便优雅。
sax 模式读取,SXSS 模式写入。避免 excel 大文件 OOM。
基于注解,编程更加灵活。
写入能够基于对象列表,也能够基于 Map,实际使用更加方便。
读取跳过空白行
实际工做和学习中,apache poi 操做 excel 过于复杂。
近期也看了一些其余的工具框架:
easypoi
easyexcel
都或多或少难以知足本身的实际须要,因而就本身写了一个操做 excel 导出的工具。
使用 maven 管理。
<dependency> <groupId>com.github.houbb</groupId> <artifactId>iexcel</artifactId> <version>0.0.2</version> </dependency>
你能够直接参考 ExcelUtilTest.java
定义一个须要写入/读取的 excel 对象。
只有声明了 @ExcelField
的属性才会被处理,使用说明:@ExcelField
public class ExcelFieldModel { @ExcelField private String name; @ExcelField(headName = "年龄") private String age; @ExcelField(mapKey = "EMAIL", writeRequire = false, readRequire = false) private String email; @ExcelField(mapKey = "ADDRESS", headName = "地址", writeRequire = true) private String address; //getter and setter }
IExcelWriter 有几个实现类,你能够直接 new 或者借助 ExcelUtil
类去建立。
IExcelWriter 实现类 | ExcelUtil 如何建立 | 说明 |
---|---|---|
HSSFExcelWriter | ExcelUtil.get03ExcelWriter() | 2003 版本的 excel |
XSSFExcelWriter | ExcelUtil.get07ExcelWriter() | 2007 版本的 excel |
SXSSFExcelWriter | ExcelUtil.getBigExcelWriter() | 大文件 excel,避免 OOM |
一个将对象列表写入 2003 excel 文件的例子。
/** * 写入到 03 excel 文件 */ @Test public void excelWriter03Test() { // 待生成的 excel 文件路径 final String filePath = "excelWriter03.xls"; // 对象列表 List<ExcelFieldModel> models = buildModelList(); try(IExcelWriter excelWriter = ExcelUtil.get03ExcelWriter(); OutputStream outputStream = new FileOutputStream(filePath)) { // 可根据实际须要,屡次写入列表 excelWriter.write(models); // 将列表内容真正的输出到 excel 文件 excelWriter.flush(outputStream); } catch (IOException e) { throw new ExcelRuntimeException(e); } }
/** * 构建测试的对象列表 * @return 对象列表 */ private List<ExcelFieldModel> buildModelList() { List<ExcelFieldModel> models = new ArrayList<>(); ExcelFieldModel model = new ExcelFieldModel(); model.setName("测试1号"); model.setAge("25"); model.setEmail("123@gmail.com"); model.setAddress("贝克街23号"); ExcelFieldModel modelTwo = new ExcelFieldModel(); modelTwo.setName("测试2号"); modelTwo.setAge("30"); modelTwo.setEmail("125@gmail.com"); modelTwo.setAddress("贝克街26号"); models.add(model); models.add(modelTwo); return models; }
有时候列表只写入一次很常见,全部就简单的封装了下:
/** * 只写入一次列表 * 实际上是对原来方法的简单封装 */ @Test public void onceWriterAndFlush07Test() { // 待生成的 excel 文件路径 final String filePath = "onceWriterAndFlush07.xlsx"; // 对象列表 List<ExcelFieldModel> models = buildModelList(); // 对应的 excel 写入对象 IExcelWriter excelWriter = ExcelUtil.get07ExcelWriter(); // 只写入一次列表 ExcelUtil.onceWriteAndFlush(excelWriter, models, filePath); }
excel 读取时会根据文件名称判断是哪一个版本的 excel。
IExcelReader 有几个实现类,你能够直接 new 或者借助 ExcelUtil
类去建立。
IExcelReader 实现类 | ExcelUtil 如何建立 | 说明 |
---|---|---|
ExcelReader | ExcelUtil.getExcelReader() | 小文件的 excel 读取实现 |
Sax03ExcelReader | ExcelUtil.getBigExcelReader() | 大文件的 2003 excel 读取实现 |
Sax07ExcelReader | ExcelUtil.getBigExcelReader() | 大文件的 2007 excel 读取实现 |
/** * 读取测试 */ @Test public void readWriterTest() { File file = new File("excelWriter03.xls"); IExcelReader<ExcelFieldModel> excelReader = ExcelUtil.getExcelReader(file); List<ExcelFieldModel> models = excelReader.readAll(ExcelFieldModel.class); System.out.println(models); }
@ExcelField
的属性说明以下:
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
mapKey | String | "" |
仅用于生成的入参为 map 时,会将 map.key 对应的值映射到 bean 上。若是不传:默认使用当前字段名称 |
headName | String | "" |
excel 表头字段名称,若是不传:默认使用当前字段名称 |
writeRequire | boolean | true | excel 文件是否须要写入此字段 |
readRequire | boolean | true | excel 文件是否读取此字段 |
/** * 写出数据,本方法只是将数据写入Workbook中的Sheet,并不写出到文件<br> * <p> * data中元素支持的类型有: * <pre> * 1. Bean,既元素为一个Bean,第一个Bean的字段名列表会做为首行,剩下的行为Bean的字段值列表,data表示多行 <br> * </pre> * @param data 数据 * @return this */ IExcelWriter write(Collection<?> data); /** * 写出数据,本方法只是将数据写入Workbook中的Sheet,并不写出到文件<br> * 将 map 按照 targetClass 转换为对象列表 * 应用场景: 直接 mybatis mapper 查询出的 map 结果,或者其余的构造结果。 * @param mapList map 集合 * @param targetClass 目标类型 * @return this */ IExcelWriter write(Collection<Map<String, Object>> mapList, final Class<?> targetClass); /** * 将Excel Workbook刷出到输出流 * * @param outputStream 输出流 * @return this */ IExcelWriter flush(OutputStream outputStream);
建立 IExcelWriter 的时候,能够指定 sheet 的下标或者名称。来指定写入的 sheet。
建立 IExcelWriter 的后,能够调用 excelWriter.containsHead(bool)
指定是否生成 excel 表头。
/** * 读取当前 sheet 的全部信息 * @param tClass 对应的 javabean 类型 * @return 对象列表 */ List<T> readAll(Class<T> tClass); /** * 读取指定范围内的 * @param tClass 泛型 * @param startIndex 开始的行信息(从0开始) * @param endIndex 结束的行信息 * @return 读取的对象列表 */ List<T> read(Class<T> tClass, final int startIndex, final int endIndex);
建立 IExcelReader 的时候,能够指定 sheet 的下标或者名称。来指定读取的 sheet。
注意:大文件 sax 读取模式,只支持指定 sheet 的下标。
建立 IExcelReader 的后,能够调用 excelReader.containsHead(bool)
指定是否读取 excel 表头。