问题
在一般的后台管理系统中都少不了导入导出功能,而且很多都是以Excel的格式进行操作。在之前的项目中用的是前辈们提供的一个工具类。总结一下这个工具类可以帮你从Excel中解析出你要的数据,也可以把你的数据转化成Excel,功能比较基础和纯粹。但是从一个完成的导入导出逻辑来说,我们要做的远不止这些东西,比如在我们的项目中,完整的导入导出应该是这样的:
- 导入:
- 用户选择文件,点击导入按钮
- 前端页面展示目前的导入进度,总条数,成功条数,错误条数
- 导入完毕,展示包含错误数据的错误文件的下载链接
- 用户下载错误文件,查看每条数据导入错误的原因,然后修改数据
- 重新导入
- 导入应该支持Excel 03版本,07版本
- 导入性能应该尽量高
- 导入最大应该支持100万数据的导入,不能出现OOM
- 导出:
- 用户点击导出按钮
- 弹出对话框让用户选择需要导出的字段
- 用户点击确定,开始导出
- 前端页面显示导出进度,总条数,当前条数
- 导出完毕,直接下载文件到本地电脑
- 导出应该支持Excel 03版本,07版本
- 导出性能应该尽量高
- 导出最大应该支持100万数据的导出,不能出现OOM
基于上面这些问题,我们原来的一个简单的工具类是没办法解决的,只能另寻他路。
引入EasyExcel
几经寻找,找到了这个阿里巴巴开源的项目,看了一下文档,下载下来把我需要的几个功能都试了一下,感觉不错。不管从可用性,稳定性,性能以及学习成本上都比我们原来的工具类强很多,引入EasyExcel之后我们可以解决上面导入导出场景中的6,7,8问题,就是说针对Excel本身的这些操作我们可以不用操心了,完全交给EasyExcel就可以了,我们自己要去解决剩下的问题。
设计导入模式
其实我们大部分的导入场景的过程都是一样的,基本上分为以下几步:
- 从Excel解析出数据,转换为Java对象
- 遍历Java对象,处理每一个Java对象,通常就是简单的插入数据库
- 导入完毕,得到导入的结果:总条数,成功条数,错误条数,错误数据列表
所以这个地方我们可以设计一个模板模式+策略模式,这样可以将导入过程统一起来,把公共的步骤提供默认实现,每个业务只需要实现自己不同的地方,就是实现怎么去处理每一个Java对象。
简单写一下伪代码:
导入模板接口:
interface ImportTemplateInterface<T>{
/**
*默认方法,实现导入模板过程
*/
default String import(InputStream ins){
//任务id,返回给客户端用户查询导入进度
String taskId = UUID.random().toString();
//异步执行
new Thread(
new Runable(){
public void run(){
updateProgress(taskId,START);
//利用easyExcel读取数据
List<T> modelList = easyExcel.read(ins);
//错误数据列表
List<ErrorModel> errorList = new ArrayList<>();
updateProgress(taskId,totalCount++);
modelList.foreach(model -> {
try{
//处理数据
importItem(model);
updateProgress(taskId,successCount++);
}catch(Exception e){
//加入到错误列表
errorList.add(toErrorModel(model));
updateProgress(taskId,errorCount++);
}
});
updateProgress(END);
}
}
).start()
return taskId;
}
/**
*处理数据方法,由具体实现类去实现
*/
void importItem(T model);
/**
*更新进度方法,默认实现类可以提供默认实现,具体实现类可以覆盖
*/
void updateProgress(String taskId,int count);
}
模板类的默认实现:
abstract class DefaultImportTemplate<T> implements ImportTemplateInterface <T>{
@Resource
private Redis redis;
/**
*抽象方法,处理数据,由子类提供实现
*/
abstract void importItem(T model);
/**
*默认的更新进度实现
*/
void updateProgress(String taskId,int count){
//进度存储到redis
redis.update(taskId,count);
}
/**
*默认的获取流程进度的方法
*/
void getProgress(String taskId){
redis.get(taskId);
}
/**
*默认的获取错误数据的方法
*/
List<ErrorModel> getErrorList(String taskId){
redis.getErrorList(taskId);
}
}
业务类的实现:
class UserImportTemplate extends DefaultImportTemplate<User> {
@Resource
private UserDao userDao;
/**
*用户导入实现处理数据的方法,插入用户
*/
void importItem(User model){
userDao.insert(model);
}
}
设计导出模式
其实导出跟导入一样也可以使用模板模式+策略模式,在导出的过程里面,主要需要业务自己实现的是获取数据的方法和获取表头的方法,其他的都可以在模板里面做好。这里可以没有默认实现,因为获取数据和获取表头都是和业务强相关的没法提供默认实现。伪代码思路基本上跟导入是一样的,不写了。
导出的动态表头设计
我们系统有点特殊的地方,就是导出的字段是可选的,那这个该怎么办呢?
设计一个注解
注解有index,value两个基础属性,index表示导出时表头字段的顺序,value表示表头的显示名称,把注解加在需要导出的实体类的字段上。
获取所有可选表头
前端传一个参数表示是哪个模块的导出操作,后端根据参数找到该模块导出的实体类,然后利用反射找出实体类中所有加了上面那个注解的字段,然后封装成表头对象<index,columnKey,columnName>,columnKey是指字段的名字,columnName是指表头显示名称,就是上面注解的value。然后把这些可选表头全部返回给前端,给用户选择。
根据选择的表头获取数据
用户在页面上勾选的表头时候提交到后端,后端获取了表头之后,使用EasyExcel的创建表头功能创建表头,这一步比较简单,就是把我们自己的表头对象转换成EasyExcel的格式,主要用到index和columnName两个属性。然后查询出需要导出的数据,因为我们查询出来的对象里面是所有属性都有值的,如果直接使用EasyExcel导出的话,会把所有数据都会导出,这样就会出现,表头是选择的那么多,但是每一行后面的数据会多出那些没有选择的列。因此,我们在从数据库获取数据之后,我们还需要利用表头中的columnKey的值进行反射获取对象的属性值,我们只获取用户选择的那些columnKey的值,然后组装成EasyExcel中需要的那种格式,然后就可以导出了。在这个过程中我们的表头和我们的内容顺序要保持一致,要不然会出现内容和表头对不上,所以我们在创建表头和内容是都要先对提交上来的可选表头按index排序,这样就OK了。用文字描述可能有点抽象,其实自己去实现的话,没有说的那么麻烦。如果你的导出表头是固定的那就很简单了,只需要使用EasyExcel的注解,然后直接查出数据,直接导出就可以了。EasyExcel提供了根据模型注解导出,也可以自己组装数据和表头,所以我们利用后面这个特点就可以实现动态表头的导出功能。
这个地方还有一点要注意的是,你要使用反射来自己获取属性值的话,那日期类型的数据你要自己格式化一下,要不然导出到Excel里面格式是不固定的,也许不是你想要的格式。
好了,写了这么多,感觉就是一个简单的导入导出功能,如果是需要做到系统里面的导入导出比较统一实现的话,可以参考一下,这样其他的伙伴在开发的时候就比较简单了,不用每个人都把这些做一遍。