程序员

可配置化的excel导入

2017-05-21  本文已影响218人  帅气小伙

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

代码所在位置
上一篇 下一篇

猜你喜欢

热点阅读