1、前言
Spring Boot下如果只是导入一个简单的Excel文件,是容易的。网上类似的文章不少,有的针对具体的实体类,代码可重用性不高;有的利用反射机制,开发了Excel导入工具类,这样方法较好,但如果数据列有物理含义的转换,或需要进行计算处理等复杂情况,难以为继。
针对上述不足之处,本文提出了一种可重用,并且具有数据处理灵活性的代码框架。
2、需求分析
导入Excel表格数据,应解决下列问题:
- 访问Excel文件,并将指定Sheet页中的数据读出来。
- 识别并支持xls和xlsx格式的Excel文件。
- 使用实体类对象列表来存放表格数据,进而可以存入数据库表或其它业务处理。
- 实体类容易更换为其它实体类,无需大量重复代码,从而可以方便支持多种内容的表格数据导入。
- 表格数据列与实体类属性之间可能存在数据转换,常见的是物理意义的转换和数据类型转换。如性别,表格中标题为“性别”的数据列的取值为字符串“男”或“女”,而实体类中对应的属性字段名为“gender”,取值为整型数“1”或“2”。
- 表格数据的标题行可能存在下列情况:
- 没有标题行,本模块不考虑支持此情况。
- 数据列标题的次序不固定,并且可能中间有它无需导入的数据列标题。
- 需要导入的数据列标题不全。分两种情况:关键数据列缺失、可选数据列缺失。
- 表格数据的数据块位置可能存在下列情况:
- 数据块可能不是从第一行第一列开始,而是有偏移。
- 数据行的列集合与标题行的列集合不一致,可能不是简单的包含关系。
- 表格数据行可能存在下列情况:
- 空行。
- 该数据行的某些列数据有问题,不能加载到实体类对象中。
- 错误信息处理:精确定位并记录数据错误信息,数据行错误,能定位到行号、列号,便于错误核查和处理。遇到数据行数据错误,记录错误信息并继续处理。
3、设计思路
综合上述功能模块的需求分析,总体设计思路如下:
- 使用泛型T来代表实体类,这样可以方便支持更多实体类。
- 泛型T代表的实体类,必需提供某些接口方法,以便实现表格数据行的载入,表格数据行的载入实体类,有一些公共的处理代码和属性,这些可以封装在Excel导入对象基类BaseImportObj中。泛型T代表的实体类继承基类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、 导入字段定义类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.3、 Excel导入对象基类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