美文网首页
20190622_系统中的Excel导入导出功能

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

作者: GogoFun | 来源:发表于2019-06-22 22:42 被阅读0次

    问题

    在一般的后台管理系统中都少不了导入导出功能,而且很多都是以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里面格式是不固定的,也许不是你想要的格式。

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

    相关文章

      网友评论

          本文标题:20190622_系统中的Excel导入导出功能

          本文链接:https://www.haomeiwen.com/subject/ilzvqctx.html