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文件导入功能模块的类关系图如下图所示:

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

4、代码实现

4.1、 导入依赖包

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

        <!-- excel-->
		<dependency>
		    <groupId>org.apache.poi</groupId>
		    <artifactId>poi</artifactId>
		    <version>3.10-FINAL</version>
		</dependency>
		<dependency>
		    <groupId>org.apache.poi</groupId>
		    <artifactId>poi-ooxml</artifactId>
		    <version>3.10-FINAL</version>
		</dependency> 

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<T extends BaseImportObj> {

	//异常信息列表,调用importExcelFile后,可以通过getLogList获取
	private List<String> importLogList = new ArrayList<String>();
		
	//开始列号,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<T> importExcelFile(Workbook workbook,Integer sheetIndex,T rowDataObj) throws Exception {
		//复位属性值
		reset();
		
    	//读取excel文件内容
    	List<String[]> 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<T> 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<String[]> getExcelData(Workbook workbook, int sheetIndex) throws Exception {
        Sheet sheet = workbook.getSheetAt(sheetIndex);
        List<String[]> data = new ArrayList<>();
        
        //第一行数据的标记
        boolean bFirstRow = false;
        //读取数据块
        //getFirstRowNum,数据区域第一行的下标,0-based
        //getLastRowNum,数据区域的最后一行的下标,0-based
        for (int i = sheet.getFirstRowNum(); i <= sheet.getLastRowNum(); i++) {
        	//取得一行数据
            Row row = sheet.getRow(i);            
            if (row == null) {
            	//空行,跳过
            	continue;            
            }
            
            if (firstColumnIdx < 0) {
            	//表示尚未读取第一行有效数据,此时读到第一行有效数据
            	//此时认为是标题行,决定了后续数据的读取范围
            	
                //getFirstCellNum,数据区域的第一列的下标,0-based
                //getLastCellNum,数据区域的最后列的下标+1,0-based
                int first = row.getFirstCellNum();
                int last = row.getLastCellNum();
            	//设置标题行的数据范围
            	firstColumnIdx = first;   
            	lastColumnIdx = last;
            	columnCount = last - first;
            	bFirstRow = true;
            }else {
            	bFirstRow = false;
            }
            
            //不管该行的数据怎么分布,总是截取标题列范围数据
            //即从firstColumnIdx开始的columnCount列数据
            String[] rowData = new String[columnCount+1];
            if (bFirstRow) {
            	//如果为第一行数据,开始列号,0-based
            	rowData[0] = "" + firstColumnIdx;
            }else {
	            //行号,记录行号,目的是为了错误定位,1-based
	            rowData[0] = "" + (i + 1);
            }
            for (int j = firstColumnIdx; j < lastColumnIdx; j++) {
            	//取得单元格
                Cell cell = row.getCell(j);
                if (cell == null) {
                	//如果单元格为空,置为空串
                	rowData[j - firstColumnIdx + 1] = "";
                	continue;
                }
                
                //设置格式内容为string,否则读取失败
                cell.setCellType(Cell.CELL_TYPE_STRING);
                rowData[j - firstColumnIdx + 1] = cell.getStringCellValue();
            }
            data.add(rowData);
        }
        return data;
    } 	
	
    /**
     * 
     * @methodName		: processDataRows
     * @description		: 处理数据行列表
     * @param data		: 数据行列表
     * @param rowDataObj:T类型的数据对象
     * @return			: T类型的数据对象列表
     *
     */
    @SuppressWarnings("unchecked")
	private List<T> processDataRows(List<String[]> data,T rowDataObj){
    	
    	List<T> dataList = new ArrayList<T>();
    	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<String> 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<T extends BaseImportObj>

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

    	//对于标题行,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<String,ImportFieldDef> 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<Integer,String> 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<String,ImportFieldDef> titleMap) {
    	//由子类重载实现    	
    }
    
    /**
     * 
     * @methodName		: fillData
     * @description		: 将数据设置到属性字段中,子类必需重载此方法
     * @param fieldName	: 字段名称
     * @param cellData	: 对应单元格数据
     *
     */
    public void fillData(String fieldName, String cellData){
    	//由子类重载实现
    }

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

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

  • columnIdxMap字段,用于存放数据列下标与属性字段名的映射关系,类型为Map<Integer,String>。导入标题行时,需保存数据列下标与属性字段的对应关系;导入数据时,根据此映射关系,可以快速地为数据列找到对应的属性字段名,然后进行数据载入处理。
  • firstColumnIdx字段,表格中数据区域开始列的列下标,下标从0开始。考虑到数据区域的开始位置有可能偏移,即第一列数据可能不在表格的第一列。保存开始列的列下标,当载入行数据发生异常时,可以定位错误的单元格的位置。

​ BaseImportObj类提供下列方法:

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

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

01	创建Map<String,ImportFieldDef>类型对象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<String,ImportFieldDef> 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<ThrowReceiveInfo> excelImportHandler = 
				new ExcelImportHandler<ThrowReceiveInfo>(); 
		
		List<ThrowReceiveInfo> 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<ThrowReceiveInfo> importUpfileExcelFile(MultipartFile upfile) {
		
		ExcelImportHandler<ThrowReceiveInfo> excelImportHandler = 
				new ExcelImportHandler<ThrowReceiveInfo>(); 
		
		if (upfile.isEmpty()){
			return null;
		}
    	//获取文件名
        String fileName = upfile.getOriginalFilename();
        //获取文件输入流
        InputStream in = null;
        try {
        	in = upfile.getInputStream();
        } catch(Exception e) {
        	//获取文件输入流失败
        	throw e;
        }

		ExcelImportHandler<ThrowReceiveInfo> excelImportHandler = 
				new ExcelImportHandler<ThrowReceiveInfo>();
        //创建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<ThrowReceiveInfo> rowDataList = getExcelData(workbook,sheetIdx,rowDataObj);
    	
    		return rowDataList;		
	}
posted @ 2021-06-19 20:47  阿拉伯1999  阅读(642)  评论(0编辑  收藏  举报