Spring Boot下的一种导入Excel文件的代码框架

1、前言

​ Spring Boot下如果只是导入一个简单的Excel文件,是容易的。网上类似的文章不少,有的针对具体的实体类,代码可重用性不高;有的利用反射机制或自定义注解,开发了Excel导入工具类,这样方法较好,但如果数据列有物理含义的转换,或需要进行计算处理等复杂情况,难以为继。
​ 针对上述不足之处,本文提出了一种可重用,并且具有数据处理的灵活性的代码框架。

2、需求分析

​ 导入Excel表格数据,应解决下列问题:

1. 访问Excel文件,并将指定Sheet页中的数据读出来。
2. 识别并支持xls和xlsx格式的Excel文件。
3. 使用实体类对象列表来存放表格数据,进而可以存入数据库表或其它业务处理。
4. 实体类容易更换为其它实体类,无需大量重复代码,从而可以方便支持多种内容的表格数据导入。
5. 表格数据列与实体类属性之间可能存在数据转换,常见的是物理意义的转换和数据类型转换。如性别,表格中标题为“性别”的数据列的取值为字符串“男”或“女”,而实体类中对应的属性字段名为“gender”,取值为整型数“1”或“2”。
6. 表格数据的标题行可能存在下列情况:
   - 没有标题行,本模块不考虑支持此情况。
   - 数据列标题的次序不固定,并且可能中间有它无需导入的数据列标题。
   - 需要导入的数据列标题不全。分两种情况:关键数据列缺失、可选数据列缺失。
7. 表格数据的数据块位置可能存在下列情况:
   - 数据块可能不是从第一行第一列开始,而是有偏移。
   - 数据行的列集合与标题行的列集合不一致,可能不是简单的包含关系。
8. 表格数据行可能存在下列情况:
   - 空行。
   - 该数据行的某些列数据有问题,不能加载到实体类对象中。
9. 错误信息处理:精确定位并记录数据错误信息,数据行错误,能定位到行号、列号,便于错误核查和处理。遇到数据行数据错误,记录错误信息并继续处理。

3、设计思路

​ 综合上述功能模块的需求分析,总体设计思路如下:

  1. 使用泛型T来代表实体类,这样可以方便支持更多实体类。
  2. 泛型T代表的实体类,必需提供某些接口方法,以便实现表格数据行的载入,表格数据行的载入实体类,有一些公共的处理代码和属性,这些可以封装在Excel导入对象基类BaseImportObj中。泛型T代表的实体类继承基类BaseImportObj,这样可以大幅度减少实体类的代码量。如果需要支持其它更多格式的导入,如CSV格式,可以对基类BaseImportObj进行扩展,继承它的实体类根据导入文件格式的需要选择需要重载的方法。
  3. 泛型T代表的实体类,其属性字段集合应包括全部需要导入的字段集合,但不必完全一致,实体类的字段可以更多,以便不影响其它业务应用。
  4. 封装一个Excel导入处理类ExcelImportHandler,处理访问Excel文件并读取指定Sheet页的数据,返回List的列表数据。ExcelImportHandler类支持泛型T代表的实体类。
  5. ExcelImportHandler类中,为了返回List的列表数据,需要创建T类型对象,为了解决类似”new T()”问题,使用克隆(clone)方法,即要求BaseImportObj实现Cloneable。
  6. 为了描述各标题是必需字段,还是可选字段,使用导入字段定义类ImportFieldDef。

​ Excel文件导入功能模块的类关系图如下图所示:

Spring Boot下的一种导入Excel文件的代码框架

​ 如上图所示,ExcelImportHandler类调用实体类T,实体类T继承BaseImportObj类,BaseImportObj类实现Cloneable接口类,实体类T和BaseImportObj类引用ImportFieldDef类。如果不同的表格数据需要导入同一个实体类数据中,如另一份表格,对”性别”数据列的取值定义不一样,可以通过实体类的子类来实现。

4、代码实现

4.1、 导入依赖包

​ 要访问Excel文件,需要引入POI依赖包:


            org.apache.poi
            poi
            3.10-FINAL

            org.apache.poi
            poi-ooxml
            3.10-FINAL

4.2、Excel文件导入处理类ExcelImportHandler类

​ ExcelImportHandler类是导入Excel文件的核心处理类,代码如下:

package com.abc.questInvest.excel;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

/**
 * @className       : ExcelImportHandler
 * @description     : Excel文件导入处理类
 *
 */
public class ExcelImportHandler {

    //异常信息列表,调用importExcelFile后,可以通过getLogList获取
    private List importLogList = new ArrayList();

    //开始列号,0-based
    private Integer firstColumnIdx;
    //结束列号+1,0-based
    private Integer lastColumnIdx;
    //标题行的列数
    private Integer columnCount;

    /**
     *
     * @methodName      : importExcelFile
     * @description     : 导入Excel文件处理
     * @param workbook  : Workbook类型对象
     * @param sheetIndex: Sheet页序号,下标从0开始
     * @param rowDataObj: T类型的空白对象
     * @return          : T类型的对象列表
     * @throws Exception    : 异常发生时,抛出
     *
     */
    public List importExcelFile(Workbook workbook,Integer sheetIndex,T rowDataObj) throws Exception {
        //复位属性值
        reset();

        //读取excel文件内容
        List data = null;
        try {
            //读取指定sheet页
            data = getExcelData(workbook,sheetIndex);
        }catch(Exception e) {
            e.printStackTrace();
            importLogList.add("用Excel打开文件失败");
            throw e;
        }

        //检查有无数据
        if (data.size() == 0) {
            //如果无数据
            importLogList.add("表格Sheet页中没有数据");
            return null;
        }

        //第一行,为标题行
        String[] arrTitle = data.get(0);
        //处理标题行
        String errorInfo = rowDataObj.inputTitles(arrTitle);
        if (!errorInfo.isEmpty()) {
            //如果有错误信息
            importLogList.add(errorInfo);
            //表示缺失必需的数据列,返回
            return null;
        }

        //处理数据行
        List dataList = processDataRows(data,rowDataObj);

        return dataList;
    }

    /**
     *
     * @methodName      : getExcelData
     * @description     : 读取excel的sheet页,将数据存入字符串数组的列表中
     * @param workbook  : excel工作簿
     * @param sheetIndex: sheet页的索引,下标从0开始
     * @return          : 字符串数组的列表,第一列为行号,1-based
     * @throws Exception: 异常发生时,抛出
     *
     */
    private List getExcelData(Workbook workbook, int sheetIndex) throws Exception {
        Sheet sheet = workbook.getSheetAt(sheetIndex);
        List data = new ArrayList<>();

        //第一行数据的标记
        boolean bFirstRow = false;
        //读取数据块
        //getFirstRowNum,数据区域第一行的下标,0-based
        //getLastRowNum,数据区域的最后一行的下标,0-based
        for (int i = sheet.getFirstRowNum(); i  processDataRows(List data,T rowDataObj){

        List dataList = new ArrayList();
        String errorInfo = "";
        //遍历data
        for (int i = 1; i < data.size(); i++) {
            //取得数据行
            String[] dataRow = data.get(i);
            //克隆一个对象
            T rowData = (T)rowDataObj.clone();
            //导入数据
            errorInfo = rowData.inputRowData(dataRow);
            if (errorInfo.isEmpty()) {
                //加入列表中
                dataList.add(rowData);
            }else {
                //如果有异常信息
                importLogList.add(errorInfo);
            }
        }

        return dataList;
    }

    /**
     *
     * @methodName      : reset
     * @description     : 复位属性值
     *
     */
    private void reset() {
        firstColumnIdx = -1;
        lastColumnIdx = -1;
        columnCount = 0;
        importLogList.clear();
    }

    /**
     *
     * @methodName      : hasError
     * @description     : 是否有导入异常信息
     * @return          : true表示有异常信息,false无
     *
     */
    public Boolean hasError() {
        return (importLogList.size() > 0);
    }

    /**
     *
     * @methodName      : getLogList
     * @description     : 获取异常信息列表
     * @return          : 异常信息列表
     *
     */
    public List getLogList(){
        return importLogList;
    }

    /**
     *
     * @methodName      : getWorkbook
     * @description     : 判断文件格式.xls/.xlsx,用Excel打开输入文件流
     * @param in        : 输入文件流
     * @param fileName  : 文件名
     * @return          : 工作薄
     *
     */
    public Workbook getWorkbook(InputStream in, String fileName) throws Exception {
        Workbook workbook = null;
        String fileType = fileName.substring(fileName.lastIndexOf("."));
        try {
            if(".xls".equals(fileType)){
                workbook = new HSSFWorkbook(in);
            }else if(".xlsx".equals(fileType)){
                workbook = new XSSFWorkbook(in);
            }else {
                throw new Exception("文件格式错误,文件后缀须为xls/xlsx");
            }
        }catch(Exception e) {
            throw new Exception("用Excel打开上传的文件流发生异常");
        }
        return workbook;
    }
}

​ ExcelImportHandler类的声明:

public class ExcelImportHandler

​ 表明ExcelImportHandler类支持泛型T,且T必需继承BaseImportObj。

​ ExcelImportHandler类包含下列属性字段:

  • importLogList字段,异常信息列表,类型为List,用于存放导入过程的异常信息。
  • firstColumnIdx字段,标题行的开始列号,下标从0开始。
  • lastColumnIdx字段,标题行的结束列号+1,下标从0开始。
  • columnCount字段,标题行的列数。

​ 标题行的列范围,用于确定有效数据区域,在此范围之外的数据将被裁剪掉。

​ ExcelImportHandler类包含下列公共方法:

  • getWorkbook方法,判断文件格式.xls/.xlsx,用Excel打开输入文件流,成功后返回Workbook对象。
  • importExcelFile方法,导入Excel文件进行处理,此为Excel数据导入的入口方法。其针对给定的Workbook对象和指定Sheet页序号,以及一个传入的泛型T类型对象,进行数据导入处理。传入的泛型T类型对象rowDataObj,是T类型对象克隆的种子。

​ importExcelFile方法的处理算法如下:

01  调用reset私有方法,复位属性值,从而允许重复调用此方法。
02  调用getExcelData方法,获取所有数据行的列表,数据行用字符串数组表示。
03  检查有无数据,如果无数据,就返回。
04  调用T类型对象rowDataObj的inputTitles方法,处理标题行数据,如果缺失必需字段数据列,则返回。
05  调用processDataRows私有方法,处理数据部分,取得T类型对象列表,并返回此列表。
  • hasError方法,指示是否有导入异常信息。
  • getLogList方法,获取导入异常信息列表。

​ ExcelImportHandler类包含下列私有方法:

  • reset方法,复位属性值。
  • getExcelData方法,读取excel的sheet页,将数据存入字符串数组的列表中。此方法将第一个数据行,做为标题行,并确定标题行的列范围,对后续行数据,使用此列范围裁剪出有效数据列,所有行数据只取有效数据列部分,从而保证数据列与标题列的对齐。
  • processDataRows方法,处理数据行列表,不含标题行。此方法的参数为数据行字符串数组的列表,以及泛型T类型对象rowDataObj,rowDataObj是T类型对象克隆的种子。针对每一行数据,克隆一个T类型对象rowData,调用rowData的inputRowData方法载入行数据,如果有载入异常,则存入异常信息列表importLogList中,有异常的行数据丢弃,正常载入行数据就的T类型对象rowData,加入返回列表中。

4.3、导入对象基类BaseImportObj类

​ BaseImportObj类代码如下:

package com.abc.questInvest.excel;

import java.util.HashMap;
import java.util.Map;

import com.abc.questInvest.entity.ThrowReceiveInfo;

/**
 * @className   : BaseImportObj
 * @description : Excel导入数据对象基类
 *
 */
public class BaseImportObj implements Cloneable{
    //数据列下标与字段名的映射表,数据列下标从0开始
    //对于一次导入的行数据,columnIdxMap不变化,不必每个对象都创建,可以共享使用
    protected Map columnIdxMap;

    //表格中数据区域的开始列号,0-based
    protected Integer firstColumnIdx;

    // ========================================================
    // ===============公共方法实现===============================
    /**
     *
     * @methodName      : inputTitles
     * @description     : 导入标题行数据
     * @param arrTitle  : 标题名数组,标题行按列序号顺序存放,第一个成员为开始列号,0-based
     * @return          : 异常信息,空串表示无异常
     *
     */
    public String inputTitles(String[] arrTitle){
        //标题名与导入字段定义对象的映射表
        Map titleMap = new HashMap();
        //调用子类重载方法,设置标题名与导入字段定义对象的映射关系
        setExcelTitles(titleMap);

        //创建columnIdxMap对象
        columnIdxMap = new HashMap();

        //对于标题行,arrTitle的第一个成员为开始列号
        firstColumnIdx = Integer.parseInt(arrTitle[0]);

        //遍历输入的标题数组,建立列下标与字段名的映射关系
        for (int i = 1; i < arrTitle.length; i++) {
            String title = arrTitle[i].trim();
            //在titleMap中查询
            if (titleMap.containsKey(title)) {
                //如果为需要导入的列,加入columnIdxMap中
                ImportFieldDef item = titleMap.get(title);
                columnIdxMap.put((Integer)i, item.getFieldName());
            }else {
                //不需要导入的数据列,skip
            }
        }

        //检查必需字段是否都存在
        //存放缺失的必需字段
        String missingTitles = "";
        for(Map.Entry item : titleMap.entrySet()) {
            ImportFieldDef fieldItem = item.getValue();
            if (fieldItem.getMandatory() == 0) {
                //可选字段,跳过
                continue;
            }
            boolean bFound = false;
            for(String subItem : columnIdxMap.values()) {
                if(subItem.equals(fieldItem.getFieldName())) {
                    //找到该字段
                    bFound = true;
                }
            }
            if (!bFound) {
                //如果必需字段缺失,加入缺失字段中
                if(missingTitles.isEmpty()) {
                    //标题名
                    missingTitles = "数据缺失关键列名 : " + item.getKey();
                }else {
                    missingTitles += "," + item.getKey();
                }
            }
        }

        return missingTitles;
    }

    /**
     *
     * @methodName      : inputRowData
     * @description     : 导入行数据
     * @param rowData   : 行数据,第一个成员为行号,1-based
     * @return          : 异常信息,空串表示无异常
     *
     */
    public String inputRowData(String[] rowData){
        String errorInfo = "";

        for(Map.Entry item : columnIdxMap.entrySet()) {
            Integer colIdx = item.getKey();
            String fieldName = item.getValue();

            //处理各个字段,可能数据会有问题
            try {
                //调用子类重载方法,载入数据到对象中
                fillData(fieldName,rowData[colIdx]);
            }catch(Exception e) {
                //异常单元格提示信息
                //对于数据行,arrTitle的第一个成员为行号
                Integer currentColIdx = colIdx + firstColumnIdx;
                //也可用A-Z表示:(char)(currentColIdx + 65 - 1),但注意超过26列情况
                if (errorInfo.isEmpty()) {
                    //由于rowData[0]为行号,因此colIdx为列号
                    errorInfo = "数据行错误, row : " + rowData[0]
                            + ", column : [" + currentColIdx;
                }else {
                    errorInfo += "," + currentColIdx;
                }
            }
        }

        if (!errorInfo.isEmpty()) {
            errorInfo += "]";
        }
        return errorInfo;
}

    /**
     *
     * @methodName      : clone
     * @description     : 克隆,用于对象的创建
     * @return          : 复制得到的对象
     *
     */
    @Override
    public Object clone() {
        ThrowReceiveInfo obj = null;
        try{
            obj = (ThrowReceiveInfo)super.clone();
        }catch(CloneNotSupportedException e){
            e.printStackTrace();
        }
        return obj;
    }

    // ========================================================
    // ===============子类需重载的方法===========================
    /**
     *
     * @methodName      : setExcelTitles
     * @description     : 设置Excel数据的标题信息,子类必需重载此方法
     * @param titleMap  : 标题名与导入字段定义对象的映射表
     *
     */
    public void setExcelTitles(Map titleMap) {
        //由子类重载实现
    }

    /**
     *
     * @methodName      : fillData
     * @description     : 将数据设置到属性字段中,子类必需重载此方法
     * @param fieldName : 字段名称
     * @param cellData  : 对应单元格数据
     *
     */
    public void fillData(String fieldName, String cellData){
        //由子类重载实现
    }

​ BaseImportObj类实现Cloneable接口类,即重载clone方法。

​ BaseImportObj类包含下列属性字段:

  • columnIdxMap字段,用于存放数据列下标与属性字段名的映射关系,类型为Map
  • firstColumnIdx字段,表格中数据区域开始列的列下标,下标从0开始。考虑到数据区域的开始位置有可能偏移,即第一列数据可能不在表格的第一列。保存开始列的列下标,当载入行数据发生异常时,可以定位错误的单元格的位置。

​ BaseImportObj类提供下列方法:

  • inputTitles方法,载入标题行数据,并返回可能错误提示信息。参数为字符串数组类型的arrTitle。约定参数arrTitle的第一个成员即arrTitle[0]为标题行Row的开始列的列下标,下标从0开始。参数arrTitle后续成员为标题行的各列值。

​ inputTitles方法的处理算法如下:

01  创建Map类型对象titleMap的临时变量,此为标题名与导入字段定义对象的映射表。
02  调用setExcelTitles方法,设置titleMap,此方法由子类重载实现。
03  创建columnIdxMap对象,columnIdxMap对象只在载入标题行时创建一次,以后不必创建。实体类对象通过clone方法,共享同一个columnIdxMap对象。
04  取得arrTitle[0],即表格数据区域的开始列下标,存入firstColumnIdx字段。
05  遍历输入的标题数组,建立数组下标与属性字段名的映射关系。此时columnIdxMap对象中设置了信息,其中只有导入需要关注的字段集。
06  检查必需字段是否都存在,如果有缺失,检查缺失哪些标题名,然后返回错误提示信息。
  • inputRowData方法,载入行数据,并返回可能错误提示信息。参数为字符串数组类型的arrTitle。约定参数arrTitle的第一个成员即arrTitle[0]为当前行号。当载入行数据发生异常时,可以定位错误的单元格的位置。

​ inputRowData方法的处理算法如下:

01  遍历columnIdxMap对象,根据数组下标和属性字段的映射关系,可以取得字段名和对应的数据值。
02  调用fillData方法,设置属性字段的值,此方法由子类重载实现。
03  如果单元格数据设置有异常,则记录下列,继续处理后续字段,该行数据如果有多个字段的数据有异常,可以一次性输出完整的错误提示信息。
04  返回错误提示信息,如果无错误提示信息,则返回空串。
  • clone方法,实现对象的克隆。该方法是BaseImportObj类实现Cloneable接口类必需重载的接口方法。

​ clone方法是Object类的方法,但该方法的声明:

protected native Object clone() throws CloneNotSupportedException;

​ 此方法被protected修饰,如果想要使用它,要使得其他类能使用这个类,需要设置成public。

​ 此处,clone方法只需实现浅拷贝,这样columnIdxMap对象引用同一个对象。

  • setExcelTitles方法,设置Excel数据的标题信息,子类必需重载此方法。
  • fillData方法,将数据设置到属性字段中,子类必需重载此方法。

4.4、 导入字段定义类ImportFieldDef类

​ ImportFieldDef类代码如下:

package com.abc.questInvest.excel;

import lombok.Data;

/**
 * @className   : ImportFieldDef
 * @description : 导入字段定义
 *
 */
@Data
public class ImportFieldDef {
    //字段名
    private String fieldName;
    //字段是否必需,1表示必需,0表示可选
    private Integer mandatory;

    public ImportFieldDef(String fieldName,Integer mandatory) {
        this.fieldName = fieldName;
        this.mandatory = mandatory;
    }
}

​ ImportFieldDef类是一个实体类,定义了2个属性字段:

  • fieldName字段,指实体类中属性字段的名称。
  • mandatory字段,表示该字段是必需字段,还是可选字段。必需字段要求数据列必需在导入表格中,可选字段,允许无相应数据。

​ ImportFieldDef类使用lombok的@Data注解,代替属性的getter/setter代码。

4.5、实体类

​ 实体类只要继承BaseImportObj类即可,这样可以对已有的任意实体类进行改造。

​ 示例的实体类名为ThrowReceiveInfo类,代码如下:

package com.abc.questInvest.entity;

import java.util.Date;
import java.util.Map;

import javax.persistence.Column;
import javax.persistence.Id;

import com.abc.questInvest.excel.BaseImportObj;
import com.abc.questInvest.excel.ImportFieldDef;

import lombok.Data;

/**
 * @className   : ThrowReceiveInfo
 * @description : 投放/回收信息类
 *
 */
@Data
public class ThrowReceiveInfo extends BaseImportObj{
    // 记录id
    @Id
    @Column(name = "rec_id")
    private Integer recId;

    // 手机号码
    @Column(name = "phone_number")
    private String phoneNumber;

    // 用户姓名
    @Column(name = "user_name")
    private String userName;

    // 用户id
    @Column(name = "user_id")
    private Integer userId;

    //========================其它属性===========================
    //.....

    // 创建时间
    @Column(name = "create_time")
    private Date createTime;

    // 更新时间
    @Column(name = "update_time")
    private Date updateTime;

    //.....

    //============================================================
    //========================Excel数据导入========================
    /**
     *
     * @methodName      : setExcelTitles
     * @description : 设置Excel数据的标题信息,子类必需重载此方法
     * @param titleMap  : 标题名与导入字段定义对象的映射表
     *
     */
    @Override
    public void setExcelTitles(Map titleMap) {
        //此处定义标题名与导入字段定义对象的映射关系
        //这是期望的标题列,实际上,标题列可能多,如包括其它不需要导入的字段;
        //标题列也可能少,如某些字段缺失;
        titleMap.put("手机号码", new ImportFieldDef("phoneNumber",1));  //必需字段
        titleMap.put("用户姓名", new ImportFieldDef("userName",0));     //可选字段
        titleMap.put("用户ID", new ImportFieldDef("userId",0));           //可选字段
    }

    /**
     *
     * @methodName      : fillData
     * @description : 将数据填充到对象中,子类必需重载此方法
     * @param fieldName : 字段名称
     * @param cellData  : 对应单元格数据
     *
     */
    @Override
    public void fillData(String fieldName, String cellData){
        switch(fieldName) {
        case "phoneNumber":
            phoneNumber = cellData;
            break;
        case "userName":
            userName = cellData;
            break;
        case "userId":
            if (cellData.isEmpty()) {
                userId = 0;
            }else {
                userId = Integer.parseInt(cellData);
            }
            break;
        default:
            break;
        }
    }
}

​ ThrowReceiveInfo类继承BaseImportObj类,其有很多属性字段,但例子中与Excel数据导入相关的属性字段只有3个:phoneNumber、userName、userId。

​ ThrowReceiveInfo类重载实现setExcelTitles和fillData方法。可以看到,代码非常简单。

​ setExcelTitles定义了表格的标题名称与属性字段的映射关系,并且表明该字段对应的数据列是否允许缺失。

​ fillData方法,提供了每个导入字段的数据处理方法。

4.6、 单元测试

​ 对ExcelImportHandler类进行单元测试,可以了解调用方法和处理效果。

​ 先构造导入的Excel表格数据文件,作为测试用例。

​ 数据如下:

Row/Col A B C 1 用户姓名 手机号码 用户ID 2 张一 13801383X38 3 李二 13901393X39 4 王三 13310234X67

​ 手机号码随意输入,仅作示意。

​ 为了说明表格中的数据区域的位置,将Sheet页的行列编号也显示出来了。将此文件存为throw_records.xls,作为基础文件。存储路径为项目根目录夹。

​ 在throw_records.xls基础上,修改标题”用户姓名”为”用户A姓名”,作为缺失可选字段的情况,另存为throw_records-1.xls。

​ 在throw_records.xls基础上,修改标题”手机号码”为”手机A号码”,作为缺失必需字段的情况,另存为throw_records-2.xls。

​ 在throw_records.xls基础上,行2列3单元格,输入”abc”,作为数据行载入异常的情况,另存为throw_records-3.xls。

​ 在throw_records.xls基础上,将数据行偏移,并插入其它无关列,如下表所示:

Row/Col A B C D E 1 2 用户姓名 性别 手机号码 用户ID 3 张一 男 13801383X38 4 李二 男 13901393X39 5 王三 男 13310234X67

​ 作为数据区域偏移,并且有多余数据列的情况,另存为throw_records-4.xls。

​ 在throw_records-4.xls基础上,行3列5单元格,输入”abc”;行5列5单元格,输入”abc”,作为数据区域偏移时异常信息的提示情况,另存为throw_records-5.xls。

​ 在throw_records-4.xls基础上,数据行部分A列输入值,行3列1单元格,输入”abc”,作为存在裁剪区域之外数据的情况,另存为throw_records-6.xls。

​ 然后编写单元测试类,代码如下:

package com.abc.questInvest.excel;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.List;

import org.apache.poi.ss.usermodel.Workbook;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import com.abc.questInvest.entity.ThrowReceiveInfo;

/**
 * @className       : ExcelImportHandlerTest
 * @description     : Excel文件导入测试
 *
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class ExcelImportHandlerTest {
    @Test
    public void importExcelFileTest() {

        ExcelImportHandler excelImportHandler =
                new ExcelImportHandler();

        List dataList = null;
        String property = System.getProperty("user.dir");
        String filePath = property + "\\throw_records.xls";

        InputStream in = null;
        try {
            in = new FileInputStream(new File(filePath));
            if(in != null) {
                ThrowReceiveInfo rowDataObj = new ThrowReceiveInfo();
                Workbook wb = excelImportHandler.getWorkbook(in, filePath);
                dataList = excelImportHandler.importExcelFile(wb, 0, rowDataObj);
                in.close();
            }
        }catch(Exception e) {
            e.printStackTrace();
        }

        if (dataList != null) {
            System.out.println(dataList);
        }
        System.out.println(excelImportHandler.getLogList());

    }
}

​ 执行测试代码,并依次替换filePath的导入Excel文件名。可以观察结果如下:

第1次测试,throw_records.xls,导入成功,无异常信息,dataList有3个成员。
第2次测试,throw_records-1.xls,导入成功,无异常信息,dataList有3个成员。
第3次测试,throw_records-2.xls,导入失败,异常信息:[数据缺失关键列名 : 手机号码]。
第4次测试,throw_records-3.xls,导入成功,dataList有2个成员,异常信息:[数据行错误, row : 2, column : [3]]。
第5次测试,throw_records-4.xls,导入成功,无异常信息,dataList有3个成员。
第6次测试,throw_records-5.xls,导入成功,dataList有2个成员,异常信息:[数据行错误, row : 3, column : [5], 数据行错误, row : 5, column : [5]],有2条异常信息。
第7次测试,throw_records-6.xls,导入成功,无异常信息,dataList有3个成员。

4.7、 结合文件上传

​ 与文件上传结合起来,即针对文件上传MultipartFile类型对象进行处理。代码如下:

    public List importUpfileExcelFile(MultipartFile upfile) {

        ExcelImportHandler excelImportHandler =
                new ExcelImportHandler();

        if (upfile.isEmpty()){
            return null;
        }
        //获取文件名
        String fileName = upfile.getOriginalFilename();
        //获取文件输入流
        InputStream in = null;
        try {
            in = upfile.getInputStream();
        } catch(Exception e) {
            //获取文件输入流失败
            throw e;
        }

        ExcelImportHandler excelImportHandler =
                new ExcelImportHandler();
        //创建Excel工作薄
        Workbook workbook = null;
        try {
            //打开Excel工作薄;
            workbook = excelImportHandler.getWorkbook(in,fileName);
        }catch(Exception e) {
            //打开Excel工作薄失败
            throw e;
        }

        //读取excel文件内容,并存入List中
        ThrowReceiveInfo rowDataObj = new ThrowReceiveInfo();
        //sheetIdx,该值也可以在文件上传时,作为参数输入
        Integer sheetIdx = 0;
        List rowDataList = getExcelData(workbook,sheetIdx,rowDataObj);

            return rowDataList;
    }

Original: https://www.cnblogs.com/alabo1999/p/14904911.html
Author: 阿拉伯1999
Title: Spring Boot下的一种导入Excel文件的代码框架

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/610024/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • easyExcel入门使用

    1 easyExcel介绍 1) easyExcel官网介绍 传统操作Excel大多都是利用Apach POI进行操作的,但是POI框架并不完善,使用过程非常繁琐且有较多的缺陷: …

    Java 2023年6月5日
    0114
  • spring boot 配置随机数那些小技

    Spring Boot 是支持在系统加载的时候配置随机数的 1.添加config/random.properties文件 #随机32位MD5字符串 user.random.secr…

    Java 2023年5月30日
    070
  • 报错One record is expected, but the query result is multiple records

    总结:出现这种情况,显而易见,就是查询的数据在数据库中不止一条,而我调用的selectOne方法,返回值是一个User对象,导致报错 点击查看错误代码 LambdaQueryWra…

    Java 2023年6月15日
    069
  • 切入点表达式

    按类匹配和按方法匹配详解 1.按类匹配:匹配到的Java类中全部方法作为目标连接点,使用within关键字。 匹配到某个类 <aop:pointcut expression=…

    Java 2023年6月9日
    072
  • SpringMVC 使用注解时路径找不到

    SpringMVC 注解路径找不到 今天在使用SpringMVC时偶然遇到了跳转 404 的问题,于是决定记录下来 启动后输入 @RequestMapping(“/lo…

    Java 2023年6月14日
    079
  • 我的 web 前端开发技术选择

    不使用 mvvm 之类的前端组件,是因为我觉得没有必要。 mvvm 常见的宣传,对我来说没什么吸引力,反而增加了技术的复杂度。 一、Javascript 操作 DOM 慢。我不觉得…

    Java 2023年6月9日
    064
  • Java两种核心机制

    1.Java虚拟机 2.垃圾回收 posted @2017-02-26 23:13 Big_Foot 阅读(250 ) 评论() 编辑 Original: https://www….

    Java 2023年5月29日
    077
  • 【设计模式】汉堡中的设计模式——观察者模式

    【设计模式】汉堡中的设计模式——观察者模式 【设计模式】汉堡中的设计模式——观察者模式 情景带入 为什么关注公众号就可以 发布者-订阅者模式与观察者模式 + 观察者模式(Obser…

    Java 2023年6月5日
    088
  • IDEA插件:快速删除Java代码中的注释

    有时,我们需要删除Java源代码中的注释。目前有不少方法,比如: 在上面的代码中,我们首先创建JavaParser,再解析源码,然后移除单行注释和块注释,最后再用LexicalPr…

    Java 2023年6月16日
    069
  • 标准输出流与日志文件

    一、PrintStream 标准的字节输出流,默认输出到控制台 1.构造方法 PrintStream(File file) 使用指定的文件创建一个新的打印流,而不需要自动换行。 P…

    Java 2023年6月9日
    069
  • JavaFx 模拟键盘和鼠标事件

    模拟键盘事件 可实现按键的模拟,包含快捷键 模拟按下 ctrl+v示例代码: val robot = Robot() robot.keyPress(KeyEvent.VK_CONT…

    Java 2023年6月13日
    076
  • 程序员如何利用技术能力变现

    本文节选左耳朵耗子相关文章,与读者共勉! 本质上来说,程序员是手艺人,有手艺的人就能做出别人做不出来的东西,而付费也是一件很自然的事了。那么,这个问题就成了,如何让自己的&#822…

    Java 2023年6月6日
    080
  • 集合笔记记录

    基础概念 Collection 是一个接口,定义了一些集合的通用方法(不做记录,自己查表)。在 Collection 中存放的是单列数据,即非 (key, value) 对形式的数…

    Java 2023年6月7日
    054
  • Gitlab-CICD最简单明了的入门教程

    CICD是什么? 由于目前公司使用的gitlab,大部分项目使用的CICD是gitlab的CICD,少部分用的是jenkins,使用了gitlab-ci一段时间后感觉还不错,因此总…

    Java 2023年6月8日
    066
  • java.util.ConcurrentModificationException异常原因和解决方法

    问题: 在对集合迭代的时候,如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常,问题重现: 原因分析: 进入报错信息…

    Java 2023年5月29日
    068
  • Nginx中的Rewrite的重定向配置与实践

    简介:Rewrite是Nginx服务器提供的一个重要的功能,它可以实现URL重定向功能。回到顶部 一:理解地址重写 与 地址转发的含义。 地址重写与 地址转发是两个不同的概念。 地…

    Java 2023年5月30日
    097
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球