20190622_系统中的Excel导入导出功能

问题

在一般的后台管理系统中都少不了导入导出功能,而且很多都是以Excel的格式进行操作。在之前的项目中用的是前辈们提供的一个工具类。总结一下这个工具类可以帮你从Excel中解析出你要的数据,也可以把你的数据转化成Excel,功能比较基础和纯粹。但是从一个完成的导入导出逻辑来说,我们要做的远不止这些东西,比如在我们的项目中,完整的导入导出应该是这样的:

  • 导入:
    1. 用户选择文件,点击导入按钮
    2. 前端页面展示目前的导入进度,总条数,成功条数,错误条数
    3. 导入完毕,展示包含错误数据的错误文件的下载链接
    4. 用户下载错误文件,查看每条数据导入错误的原因,然后修改数据
    5. 重新导入
    6. 导入应该支持Excel 03版本,07版本
    7. 导入性能应该尽量高
    8. 导入最大应该支持100万数据的导入,不能出现OOM
  • 导出:
    1. 用户点击导出按钮
    2. 弹出对话框让用户选择需要导出的字段
    3. 用户点击确定,开始导出
    4. 前端页面显示导出进度,总条数,当前条数
    5. 导出完毕,直接下载文件到本地电脑
    6. 导出应该支持Excel 03版本,07版本
    7. 导出性能应该尽量高
    8. 导出最大应该支持100万数据的导出,不能出现OOM

基于上面这些问题,我们原来的一个简单的工具类是没办法解决的,只能另寻他路。

引入EasyExcel

几经寻找,找到了这个阿里巴巴开源的项目,看了一下文档,下载下来把我需要的几个功能都试了一下,感觉不错。不管从可用性,稳定性,性能以及学习成本上都比我们原来的工具类强很多,引入EasyExcel之后我们可以解决上面导入导出场景中的6,7,8问题,就是说针对Excel本身的这些操作我们可以不用操心了,完全交给EasyExcel就可以了,我们自己要去解决剩下的问题。

设计导入模式

其实我们大部分的导入场景的过程都是一样的,基本上分为以下几步:

  1. 从Excel解析出数据,转换为Java对象
  2. 遍历Java对象,处理每一个Java对象,通常就是简单的插入数据库
  3. 导入完毕,得到导入的结果:总条数,成功条数,错误条数,错误数据列表

所以这个地方我们可以设计一个模板模式+策略模式,这样可以将导入过程统一起来,把公共的步骤提供默认实现,每个业务只需要实现自己不同的地方,就是实现怎么去处理每一个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里面格式是不固定的,也许不是你想要的格式。

好了,写了这么多,感觉就是一个简单的导入导出功能,如果是需要做到系统里面的导入导出比较统一实现的话,可以参考一下,这样其他的伙伴在开发的时候就比较简单了,不用每个人都把这些做一遍。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容