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