可配置化的excel导入

Excel导入在实际的开发中,经常会用到,可是我们往往需要研究一番这个Excel表格的结构才能够着手写Excel导入的代码,而这份代码有不能够复用,常常导致一表一码的情况,为了提高开发效率,因此写了些代码和解决思路

大前提

每一份Excel表格都有唯一标示某列或某行业务含义的标示,例如列名,某单元格特定字符。

这是一份教学进度表

很明显根据某些特定标示,我是能够知道哪个单元格是我想要的数据

解决思路

1.如何让程序根据某一字段标识与导入实体类字段存在对应关系,找出数据所在位置?
例如给出如下对应关系(参照上图教学进度的,给出如下配置)

        List<ImportField> fields = new ArrayList<>();
        //公用字段
        fields.add(new ImportField("课程名称","name"));
        fields.add(new ImportField("专业","major"));
        fields.add(new ImportField("年级","grade"));
        fields.add(new ImportField("班级","classes"));
        //多值字段
        fields.add(new ImportField("授课方式","type",true,1,0));
        fields.add(new ImportField("授课教师","teacher",true,1,0));
        fields.add(new ImportField("授课内容","content",true,1,0));
        fields.add(new ImportField("上课地点","address",true,1,0));
        fields.add(new ImportField("周次","week",true,1,0));
        fields.add(new ImportField("星期","day",true,1,0));
        fields.add(new ImportField("节次","scope",true,1,0));

2.如何通过给定的映射关系遍历Sheet和得到某一标识所在行和列?
首先我们需要定义一个导入配置,然后程序就是根据这一个导入配置去取数据

例如给出如下实体类

public class ImportField {
    //别名
    private String alias;
    //数据库字段名称或者实体类名称
    private String name;
    //是否多个
    private boolean isMulti = false;
    //行偏移量
    private int xOffset=0;
    //列偏移量
    private int yOffset=0;
    //起始行
    private int row;
    //起始列
    private int col;
    //多值属性记录数
    private int multiCount = 0;
    //是否分析得到起始行列位置
    private boolean isComplete = false;

    public ImportField() {

     }

    public String getAlias() {
        return alias;
    }

    public void setAlias(String alias) {
        this.alias = alias;
    }

    public boolean isMulti() {
        return isMulti;
    }

    public void setMulti(boolean multi) {
        isMulti = multi;
    }

    public int getxOffset() {
        return xOffset;
    }

    public void setxOffset(int xOffset) {
        this.xOffset = xOffset;
    }

    public int getyOffset() {
        return yOffset;
    }

    public void setyOffset(int yOffset) {
        this.yOffset = yOffset;
    }
    //计算所在列是需要加上行偏移量  
    public int getRow() {
        return row+xOffset;
    }

    public void setRow(int row) {
        this.row = row;
    }
    //计算所在列是需要加上列偏移量  
    public int getCol() {
        return col+yOffset;
    }

    public void setCol(int col) {
        this.col = col;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public ImportField(String alias, String name, boolean isMulti, int xOffset, int yOffset) {
        this.alias = alias;
        this.name = name;
        this.isMulti = isMulti;
        this.xOffset = xOffset;
        this.yOffset = yOffset;
    }

    public ImportField(String alias, String name) {
        this.alias = alias;
        this.name = name;
    }

    public void setPosition(int row, int col){
        this.col = col;
        this.row = row;
    }

    public boolean isComplete() {
        return isComplete;
    }

    public void setComplete(boolean complete) {
        isComplete = complete;
    }

    public void getNextRow(){
        this.row ++;
    }

    public void addMultiCount(){
        this.multiCount++;
    }

    public int getMultiCount() {
        return multiCount;
    }

遍历Excel得到标识位置所在行列

 //分析即将导入的文件
 /**
 * HashMap中包含
 *List<ImportField> fields
 *HSSFSheet
 */
private static HashMap<String,Object> analysisExcel(
InputStream inputStream, List<ImportField> fields)
 throws IOException,FormatException{
        HashMap<String,Object> analysisResult = new HashMap<>();
        int progress = 0;//扫描进度
        HSSFWorkbook hssfWorkbook = new HSSFWorkbook(inputStream);
        HSSFSheet hssfSheet = hssfWorkbook.getSheetAt(0);//默认获取第一个
        if (hssfSheet == null) {
            throw new FormatException("读取模板文件为空!");
        }
        for (int rowNum = 1; rowNum <= hssfSheet.getLastRowNum(); rowNum++) {
            HSSFRow hssfRow = hssfSheet.getRow(rowNum);
            if(hssfRow == null){
                continue;
            }
            for(int colNum = 0; colNum<hssfRow.getPhysicalNumberOfCells(); colNum++){
                HSSFCell cell = hssfRow.getCell(colNum);
                if(cell != null && StringUtils.isNoneBlank(cell.getStringCellValue())){
                    String val = cell.getStringCellValue().trim();
                    for(ImportField field : fields){
                        if(val.indexOf(field.getAlias()) != -1){
                            field.setPosition(cell.getRowIndex(),cell.getColumnIndex());
                            field.setComplete(true);
                            progress++;
                            if(progress == fields.size()){
                                //如果所需字段的位置都找到了,提前结束循环
                                analysisResult.put(FIELDS,fields);
                                analysisResult.put(SHEET,hssfSheet);
                                return analysisResult;
                            }
                        }
                    }

                }

            }
        }
        //如果没有提前跳出遍历,那么就是证明某个标识在Excel中没找到
        String errorMsg = "";
        for (ImportField field : fields){
            if(!field.isComplete()){
                errorMsg = errorMsg+" "+field.getAlias();
            }
        }
        throw new FormatException("导入课程表格式错误!不存在列"+errorMsg);
    }

3.读取数据,返回指定实体类集合
通过以上的遍历,可以得到含有行列位置的导入配置和一个Sheet对象

//导入excel
private static List<Map<String,String>> importExcel(HashMap<String,Object> analysisResult) 
throws FormatException{
        List<ImportField> fields = (List<ImportField>) analysisResult.get(FIELDS);
        HSSFSheet hssfSheet = (HSSFSheet) analysisResult.get(SHEET);

        if(fields == null || hssfSheet == null){
            throw new FormatException("分析导入文件异常");
        }
        //按行大小排序,因为是从第一行开始遍历的读取的,因此排序可以优先读取,不用再倒回来读取
        fields.sort(new Comparator<ImportField>() {
            @Override
            public int compare(ImportField o1, ImportField o2) {
                return o1.getRow()-o2.getRow();
            }
        });
        //获取公共属性和多值属性
        HashMap<String,String> singleFields = new HashMap<>();
        HashMap<String,List<String>> multiFields = new HashMap<>();

        for (int rowNum = 1; rowNum <= hssfSheet.getLastRowNum(); rowNum++) {
            HSSFRow hssfRow = hssfSheet.getRow(rowNum);
            if(hssfRow == null){
                continue;
            }
            for(int colNum = 0; colNum<hssfRow.getPhysicalNumberOfCells(); colNum++){
                HSSFCell cell = hssfRow.getCell(colNum);
                if(cell == null){
                    continue;
                }
                String val =  cell.getStringCellValue();

                for(ImportField field : fields){
                    if (field.getRow() == rowNum && field.getCol() == colNum){
                        if(!field.isMulti()){
                            singleFields.put(field.getName(),val);
                        }else{
                            //excel表格在A4纸出现第二页的情况下处理如下
                            if(val.indexOf(field.getAlias()) == -1 && StringUtils.isNoneBlank(val)){
                                if(!multiFields.containsKey(field.getName())){
                                    List<String> vals = new ArrayList<>();
                                    vals.add(val);
                                    multiFields.put(field.getName(),vals);
                                }else{
                                    multiFields.get(field.getName()).add(val);
                                }
                                field.addMultiCount();
                            }
                            field.getNextRow();//跳出循环后读取下一行
                        }
                        break;
                    }
                }
            }
        }
        //至此我们得到了2个HashMap
        //公用属性和多值属性
        //那么每一个多值属性的数量应该是相等的,否则这份Excel表格的据就不合格的。
        int multiCount = 0;
        String errorMsg ="";
        String currentCol="";//选取的某一个多记录属性字段
        for(ImportField field : fields){
            if (field.isMulti()){
                if(multiCount == 0){
                    currentCol = field.getAlias();
                    multiCount = field.getMultiCount();
                }else {
                    if(field.getMultiCount() != multiCount){
                        errorMsg = errorMsg +","+field.getAlias()+"记录数为:"+field.getMultiCount();
                    }
                }

            }
        }
        if(StringUtils.isNoneBlank(errorMsg)){
            throw new FormatException("导入课程表列["+errorMsg.substring(1)+"]与["+currentCol+"]记录数:"+multiCount+"不一致");
        }
      
        //将公用属性放入多值属性
        //例如
        //公用字段是
        //{“name”:"小明"}
        //多种字段是
        //{"phone":" [电话1,电话2,电话3]"}
        //结合之后变成
        //{[{“name”:"小明","phone":"[电话1]"},{“name”:"小明","phone":"[电话2]"},{“name”:"小明","phone":"[电话3]"}]}
        for(ImportField field : fields){
            if(!field.isMulti()){
                List<String> vals = new ArrayList<>();
                for(int i=0;i<multiCount;i++){
                    vals.add(singleFields.get(field.getName()));
                }
                multiFields.put(field.getName(),vals);
            }
        }
        List<Map<String,String>> list = new ArrayList<>();
        for(int i=0;i<multiCount;i++){
            HashMap<String,String> data = new HashMap<>();
            for(String key : multiFields.keySet()){
                List<String> vals = multiFields.get(key);
                data.put(key,vals.get(i));
            }
            list.add(data);
        }
        return list;
    }

4.通过上面的读取可以得到List<HashMap<String,String>>,下面的问题就是HashMap转实体类对象的问题了,我推荐一下这种方式

        <dependency>
            <groupId>net.sf.dozer</groupId>
            <artifactId>dozer</artifactId>
            <version>5.5.1</version>
        </dependency>

然后简单封装一下

public class BeanMapper {
    private static DozerBeanMapper dozer = new DozerBeanMapper();

    public BeanMapper() {
    }

    public static <T> T map(Object source, Class<T> destinationClass) {

        return dozer.map(source, destinationClass);
    }

    public static <T> List<T> mapList(Collection sourceList, Class<T> destinationClass) {
        ArrayList destinationList = Lists.newArrayList();
        Iterator i$ = sourceList.iterator();

        while(i$.hasNext()) {
            Object sourceObject = i$.next();
            Object destinationObject = dozer.map(sourceObject, destinationClass);
            destinationList.add(destinationObject);
        }
        return destinationList;
    }

    public static void copy(Object source, Object destinationObject) {
        dozer.map(source, destinationObject);
    }
}

然后一句话转完
List<T> list = BeanMapper.mapList(data,target);

总结

第一次用markdown写文章,感觉倍爽,代码应该还有不完善的地方,其实最好的方式是自定义一个注解,然后加上校验,可是由于知识水平的限制,暂时没有什么进展。
完整的代码
https://github.com/Mygraduate/Supervisor_Java.git

代码所在位置

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,561评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,263评论 25 707
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,335评论 0 17
  • 今早同学在群里说刚下完雨空气很好,赶紧起床去吸两口,玩笑话!也是当前的真实写照,记得小时候的环境,有雾的天气实属...
    小泥壶阅读 150评论 2 2
  • 刚刚梦到和你聊微信, 你说,你快要结婚了。 未婚妻是我认识的学姐, 我打破脑袋想了想, 就是那个, 发型像鲁豫, ...
    造作生生阅读 368评论 7 2