数据导入导出是应用系统常见需求,而excel是最重要的辅助工具,特别是数据导入。数据导出场景excel用的也相对最多,其他导出格式,如word、pdf,通常用于线下打印或电子传递,用报表工具处理更合适。
java领域操作excel组件库主要是EasyPoi,功能比较齐备,但是用起来比较麻烦,需要额外做一些定制化开发工作,并且据说存在若干BUG以及性能问题。
阿里在EasyPoi基础上做了二次封装和优化,使用起来更方便。
EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。
虽然EasyExcel提供了直接的导出Excel的api,但是关键问题有两个,一是Excel的格式,在程序中设置相当地繁琐,并且不够美观,如涉及到复合表头、合并单元格,那么不是一般的复杂,另一方面,需要在视图模型类上加诸多的注解,来控制是否显示以及必要的格式转换,这两点的初始化工作,以及调整的工作量都偏大,灵活性也太差。
从设计角度考虑,数据和展现应该是分离的,不应该耦合在一块,因此使用EasyExcel的填充api,即先人工编辑好excel模板,设置好样式,以及数据的占位,具体的数据,由应用程序后端动态生成来填充,各司其职,提供灵活性和扩展性。
如何处理数据导出
不同的场景下,导出的数据量是不同的,如系统的主数据组织机构、人员等,数据量有限,可以一次性读取到内存,一次性写入到Excel中,但是不可避免存在导出大量业务单据的情况,例如几万甚至几十万的数据,这时候,应该分批读取和分批写入,避免大量占用应用服务器内存,也减少全量垃圾回收次数,使应用运行更稳定。
有两种方案,一是平台封装两个方法,分别是一次性处理和分批处理,由业务功能开发时根据估算数据量,来自行决定调用哪一个;二是平台只提供一个方法,内部根据数据量大小及配置来决定是一次性处理还是分批处理。
经考虑后,采取以方案2,这样使用方无需过多关注细节,只是去调用导出即可,而且某些业务单据可能上百万,但是用户选择了时间段等过滤条件后,数据量可能只有几百几千条,同样更适合一次性处理。
实现阶段进行方案优化,在控制器层,父类获取获取数据量比较困难(技术上能拿到,但需要每个子类去实现获取总量的方法),因此采用变通的处理方式,即统一使用分批处理的模式,每次处理数据量设置一个较大值,比如10000,这样小数据量的导出,处理一次就结束了,而大数据量,仍会分批多次处理。
需要导出excel的功能的业务实体对应的控制器,只需要继承控制器父类,并覆写一个获取分页数据的方法即可。
并且可以自行控制单次处理数据量(如列特别多,逻辑处理复杂等)
导入功能的几个核心点:
1.首先必须有模板,并且尽可能与导出功能公用1个模板(个别业务情况下需分别使用不同模板)
2.需要进行数据字典的转换
3.需要进行数据验证
EasyExcel的实现机制是读取每行数据,然后送给监听器进行数据处理,包括验证、转换和保存。
上传文件和数据导入是否要分为两个动作?
从功能角度考虑,这是两个动作,从业务角度考虑,主要目的是导入数据,上传文件只是辅助工作或者其中1个步骤,数据导入失败往往是因为数据存在问题,需要修改后重新上传,如拆分为两个动作,则需要临时存储,以及即时清理,从用户体验角度,点击导入按钮,选择excel文件,开始上传,并导入数据,更直观方便。
实现时遇到的问题,使用element ui的uploader控件,上传文件只能执行一次,再次上传(数据验证失败,修改后重新导入或多次导入),因为缓存问题会无操作响应(不是浏览器假死,而是不触发操作),需要在上传成功和失败的情况下,均执行清理文件操作。
读不到数据?
能解析到行,但不能读取到属性值,网上各种需要继承父类、设置索引顺序等答案均不靠谱,根本原因在于视图对象类上加了lombok的链式注解
@Accessors(chain = true),这个注解将使属性的set方法返回值是对象本身,而EasyExcel组件解析处理映射使用了BeanMap从Map拷贝到Bean, 需要Map 的Key与Bean的变量名一致, 并有对应的 set方法, 且set方法为 void, 才能拷贝成功。
详见https://www.yuque.com/easyexcel/doc/read#67f86d2c
日期类型处理如何处理
EasyExcel不支持LocalDateTime和LocalData,需要自定义转换器,并且注意,excel的单元格格式设置为文本还是日期,需要使用不同的转换器,并在视图对象类上制定指定转换器注解属性。
数据验证如何处理
理论上,excel导入数据与用户通过表单录入数据是类似的,可使用同样的数据验证,但实际上还是有差异,表单录入有控件支撑,例如数据字典,传给后台的直接就是编码,而不是名称,而在excel中,用户输入的通常只能是名称,需要将名称转换为编码后存库,为方便数据转换,有一些变通的作法,例如在用户录入的excel模板上编辑好下拉列表,将编码和名称的拼接作为下拉项(如:用户状态表示为正常| NORMAL),处理时直接截取即可,这种方式的优点是不需要做额外的查询和转换,但是也仅仅适用于数据字典,如对于关联的外部对象,如用户的组织机构,发票对应的合同,必须进行额外的转换处理。
当前遇到的问题是,通过表单新增用户,性别是必填项,使用单选按钮组控件,传给后端的只有编码值,后端数据验证时通过在视图对象类上写注解,全局统一验证;而通过excel导入的,只有名称信息
目前能想到的方案是使用分组验证,对于表单录入和excel导入两种场景分别验证。
单条处理还是批量处理
EasyExcel提供的范例是解析出数据后,放到一个集合中,到了指定数据量,如300条,批量存库,这样从技术角度而言性能更高,但是用户通过excel整理的数据通常不规范,需要系统给出友好出错提示,具体是哪一行的那个属性有问题,此外,系统框架自身在创建业务实体时有处理逻辑(新增前验证是否为空、是否重复、是否存在,新增后触发相关对象的处理等),而且,用户通过excel导入的通常是小批量的主数据,几十条以内,至多几百条,成千上万罕见,因此调整为每次只处理1条数据。