EasyExcel自定义字段导入

1.背景

原先的导入功能只支持使用固定模板导入,模板格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Getter
@Setter
@ToString
public class TestCaseExcelData {

@ExcelProperty(value = "所属功能模块")
private String module;

@ExcelProperty(value = "用例编号")
private String code;

@NotBlank(message = "必填项不能为空")
@ExcelProperty(value = "*用例名称")
private String name;

@NotBlank(message = "必填项不能为空")
@ExcelProperty(value = "*优先级")
private String caseLevel;

@NotBlank(message = "必填项不能为空")
@ExcelProperty(value = "*用例类型")
private String caseType;

@ExcelProperty(value = "用例标签")
private String tags;

@ExcelProperty(value = "前置条件")
private String preSteps;

@NotBlank(message = "必填项不能为空")
@ExcelProperty(value = "*操作步骤/场景描述")
private String stepDesc;

@NotBlank(message = "必填项不能为空")
@ExcelProperty(value = "*预期结果")
private String expectResult;

@ExcelProperty(value = "关联需求类型")
private String requirementType;

@ExcelProperty(value = "关联需求ID")
private String requirementId;

@ExcelProperty(value = "用例版本")
private String caseVersion;

}

EasyExcel 导入监听器直接使用AnalysisEventListener即可实现导入校验,校验规则较为复杂,不在此处展开。
现在要求用户配置了自定义字段之后,还可以导入自定义字段,同时保留对固定字段的校验逻辑。因此原有的适用对象的监听器不再适用,需要使用无对象的方式做数据校验。

2.问题

  • EasyExcel 无对象方式的监听器是继承AnalysisEventListener<Map<Integer, String>>类,在重写了invoke() 方法后发现,入参是 Map<Integer, String> data,这就导致我无法对每一行数据按照原有的方式校验。
  • 用户导入的模板列顺序是不固定的,因此也没法遍历 data 进行原有规则的校验。

3.解决方案

3.1 解决思路

  • 既然 invoke() 方法入参是 Map<Integer, String> data 这种数据结构,那能不能把这个 Map 中固定的字段转为一个 TestCaseExcelData 对象来处理?
  • 如果要转为一个对象,那怎么把 Map 中的数据跟对象的字段做映射?

3.2 Map 转对象

  • Map<Integer, String> 是当前行的数据,其中 key 是当前行的列索引,value 是当前单元格的值,如果要转对象,首先得知道这个单元格对应的表头是什么,获取表头的方式很简单,直接在 listener 中定义一个 Map<Integer, String> headMap ,然后重写 invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) 方法,即可获取到表头。
  • 取到表头之后,就可以在 invoke(Map<Integer, String> data, AnalysisContext context)方法中遍历data,根据此 map 的 key 来获取到当前单元格表头信息。代码如下:
    1
    2
    3
    4
    5
    6
    7
    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {
    data.forEach((index, value) -> {
    // 获取表头
    String headName = headMap.get(index);
    });
    }
  • 取到了当前单元格的对应的表头之后,发现这个表头就是 TestCaseExcelData 类中属性上加的 @ExcelProperty(value = “用例版本”) 注解中 value 属性的值,那就简单了,直接通过反射获取这个类所有表头和对应的属性,然后存到一个 Map<Stirng, Field> fieldStringMap 中就好了,这样就能通过表头获取到这个表头字段对应的类属性,为我们后面创建对象奠定了基础。代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    Field[] fields = TestCaseExcelData.class.getDeclaredFields();
    for (Field field : fields) {
    if (field.isAnnotationPresent(ExcelProperty.class)) {
    ExcelProperty declaredAnnotation = field.getDeclaredAnnotation(ExcelProperty.class);
    String headValue = declaredAnnotation.value()[0];
    this.fieldStringMap.put(headValue, field);
    }
    }
  • 经过上面的几步操作,我们已经得到了如下的几个Map
    1
    2
    3
    4
    5
    6
    // 当前行的数据 <列索引, 单元格值>
    Map<Integer, String> data;
    // 表头的数据 <列索引, 单元格值>
    Map<Integer, String> headMap;
    // 实体对象表头和对应字段的数据 <表头名称, 表头对应的属性>
    Map<Stirng, Field> fieldStringMap;
  • 后面的思路经很清晰了,遍历行数据Map<Integer, String> data ,通过 key 来确定
    当前单元格对应的表头,然后通过表头来获取实体类对应的属性,再通过反射来给这个属性赋值。代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {
    // 创建实体对象
    TestCaseExcelData rawData = new TestCaseExcelData();
    try {
    data.forEach((index, value) -> {
    // 获取到当前单元格的表头
    String headName = headMap.get(index);
    // 根据表头获取实体类的属性
    Field field = fieldStringMap.get(headName);
    try {
    // 判断实体类是否有此属性
    if (field != null) {
    field.setAccessible(true);
    // 通过反射直接赋值
    field.set(rawData, value);
    }
    } catch (IllegalAccessException e) {
    throw new RuntimeException(e);
    }
    // 解析自定义字段,只有系统配置的字段才会被缓存
    List<CustomFieldPO> customFieldPOS = systemCustomFieldMap.get(headName);
    if (CollectionUtils.isNotEmpty(customFieldPOS)) {
    customFieldMap.put(customFieldPOS.get(0).getFieldKey(), value);
    }
    });
    // 固定字段校验
    ExcelValidateHelper.validateEntity(rawData);
    } catch (NoSuchFieldException e) {
    e.printStackTrace();
    }
    }
  • 经过以上操作,我们成功的把一个 Map 转为了一个已知的对象,这样就跟通过对象导入一样了,后面校验的代码也无需再重复编写。

4. 其他

最后,附上自定义模板校验表头的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
super.invokeHeadMap(headMap, context);
// 限制文件行数不超过5000行
if (context.readSheetHolder().getApproximateTotalRowNumber() > 5000) {
throw new ServiceException(CommonException.EXCEL_ROW_EXCEEDED);
}
// 校验excel模版是否正确
ExcelImportUtil.validateHeadLoosely(headMap, this.dynamicCaseHeader.get(0));
this.headMap = headMap;
Field[] fields = TestCaseExcelData.class.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(ExcelProperty.class)) {
ExcelProperty declaredAnnotation = field.getDeclaredAnnotation(ExcelProperty.class);
String headValue = declaredAnnotation.value()[0];
this.fieldStringMap.put(headValue, field);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 表头宽松校验
* <p>只校验表头是否存在模板中的字段</p>
* <p>不允许存在重复的表头</p>
* <p>导入文件中可以包含多余的列名</p>
*
* @param headMap 实际读到的表头
* @param expectedHeadMapFiled 期望的表头
*/
public static void validateHeadLoosely(Map<Integer, String> headMap, List<String> expectedHeadMapFiled) {
try {
if (CollectionUtils.isEmpty(expectedHeadMapFiled) || MapUtils.isEmpty(headMap)) {
throw new ServiceException(CommonException.EXCEL_TEMPLATE_IS_NOT_CORRECT);
}
// 移除没有内容的表头
headMap.entrySet().removeIf(entry -> entry.getValue() == null);
// 判断是否存在重复列
Collection<String> headValues = headMap.values();
Set<String> headValuesSet = new HashSet<>(headValues);
if (headValues.size() != headValuesSet.size()) {
throw new ServiceException(CommonException.EXCEL_HEADS_DUPLICATED);
}
// 判断模板字段是否都包含在表头里
for (String value : expectedHeadMapFiled) {
if (!headMap.containsValue(value)) {
throw new ServiceException(CommonException.EXCEL_TEMPLATE_IS_NOT_CORRECT);
}
}
} catch (Exception e) {
throw new ServiceException("Excel表头校验失败,异常详情:" + ExceptionUtil.getErrorMessage(e));
}
}