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;
}

Spring Boot下的一种导入Excel文件的代码框架的更多相关文章

  1. Spring Boot下的一种导出Excel文件的代码框架

    1.前言 ​ 在Spring Boot项目中,将数据导出成Excel格式文件是常见的功能.与Excel文件导入类似,此处也用代码框架式的方式实现Excel文件导出,使得代码具有可重用性,并保持导出数据 ...

  2. Spring Boot下的一种导出CSV文件的代码框架

    1.前言 ​ CSV,逗号分隔值(Comma-Separated Values),即为逗号分隔的文本文件.如果值中含有逗号.换行符.制表符(Tab).单引号及双引号,则需要用双引号括起来:如果值中包含 ...

  3. Java解析导入Excel文件后台代码实现

    使用MultipartFile上传Excel文件后端代码实现:(springmvc下的spring-webmvc (MultipartFile )上传) 由于POST一个包含文件上传的Form会以mu ...

  4. 创建Spring Boot项目的几种方式总结

    一.我们可以使用Spring Initializr来创建SpringBoot项目. Spring Initializr从本质上来说就是一个Web应用程序,它能为你生成Spring Boot项目结构.虽 ...

  5. Spring Boot下Druid连接池+mybatis

      目前Spring Boot中默认支持的连接池有dbcp,dbcp2, hikari三种连接池.  引言: 在Spring Boot下默认提供了若干种可用的连接池,Druid来自于阿里系的一个开源连 ...

  6. Spring Boot2 系列教程(二)创建 Spring Boot 项目的三种方式

    我最早是 2016 年底开始写 Spring Boot 相关的博客,当时使用的版本还是 1.4.x ,文章发表在 CSDN 上,阅读量最大的一篇有 43W+,如下图: 2017 年由于种种原因,就没有 ...

  7. spring boot下为配置属性值加密的正确姿势

    最近做电商系统,安全性要求比较高,针对配置属性值的加密自然也是需要增强的点之一,那么如何加密呢? 网上搜索了些,有jasypt加密mysql密码的最为普遍,可惜问题就在于只能加密mysql信息,其他的 ...

  8. .Net MVC 导入导出Excel总结(三种导出Excel方法,一种导入Excel方法) 通过MVC控制器导出导入Excel文件(可用于java SSH架构)

    .Net MVC  导入导出Excel总结(三种导出Excel方法,一种导入Excel方法) [原文地址] 通过MVC控制器导出导入Excel文件(可用于java SSH架构)   public cl ...

  9. spring boot下使用logback或log4j生成符合Logstash标准的JSON格式

    spring boot下使用logback或log4j生成符合Logstash标准的JSON格式 一.依赖 由于配置中使用了json格式的日志输出,所以需要引入如下依赖 "net.logst ...

随机推荐

  1. 【2020BUAA软件工程】个人博客作业

    个人作业博客 项目 内容 北航2020软工 班级博客 作业要求 具体要求 我的课程目标 学习软件工程,掌握团队合作,锻炼自我 作业在哪个方面帮助我实现目标 通读<构建之法>,尝试理解软件工 ...

  2. 【BUAA软工】Alpha阶段测试报告

    vLab-online项目Alpha阶段测试报告 项目 内容 班级:北航2020春软件工程 博客园班级博客 作业:Alpha阶段测试报告 测试报告 测试发现的bug 在测试过程中发现了多少Bug? 我 ...

  3. 企业更需要定制化CRM系统满足个性化需求

    随着市场的发展和信息技术的进步,越来越多的企业购买CRM客户关系管理系统来帮助管理.提高效率.但哪怕处在相同行业的企业,他们对于CRM的功能需求都会有着很大的不同.因此,大部分企业都开始进行个性化定制 ...

  4. MyBatis进阶--接口代理方式实现Dao 和动态SQL

    MyBatis接口代理方式实现Dao层 接口代理方式-实现规则 传统方式实现Dao层,我们既要写接口.还要写实现类.而MyBatis框架可以帮助我们省略写Dao层接口实现类的步骤.程序员只需要编写接口 ...

  5. [BD] HBase

    NoSQL数据库 关系型数据库:用表格的行-列来保存数据,OLTP,写入多,行式存储 非关系型数据库:只用来存储数据,业务逻辑由应用程序处理,OLAP,查询多,列式存储 常见NoSQL数据库 Redi ...

  6. 【转载】Python 代码调试技巧

    https://www.ibm.com/developerworks/cn/linux/l-cn-pythondebugger/ Python 代码调试技巧 张 颖2012 年 5 月 03 日发布 ...

  7. 文件不同 diff --brief XX YY

    文件不同 diff --brief XX YY 文件不同 --哪些位置 diff -c XX YY 9.diff命令 diff命令用于比较多个文本文件的差异,格式为"diff [参数] 文件 ...

  8. Java 中 volatile 关键字及其作用

    引言 作为 Java 初学者,几乎从未使用过 volatile 关键字.但是,在面试过程中,volatile 关键字以及其作用还是经常被面试官问及.这里给各位童靴讲解一下 volatile 关键字的作 ...

  9. 2.1 CPU 上下文切换(上)

    cpu上下文与切换 进程在竞争 CPU 的时候并没有真正运行,为什么还会导致系统的负载升高呢?CPU 上下文切换就是罪魁祸首. 我们都知道,Linux 是一个多任务操作系统,它支持远大于 CPU 数量 ...

  10. kylin聚合组的使用案例

    在使用kylin聚合组进行优化的时候,往往不知道怎么使用. 这里有2个小案例可以参考. 1. 需要对某个维度进行隔离. 如果有些维度,你不想要它和其他维度一起出现,你就可以单独给它建一个聚合组,并且在 ...