easyExcel实现列名多值匹配
20241108更新,偶尔会失效,原因未知,暂不推荐使用,仅供参考
1.问题
easyExcel在读取excel时,如果在实体类中的@ExcelProperty注解内添加了多个value,实际只会有第一个生效,示例:
@Data
public class MyDto implements Serializable {
@ExcelProperty(value = {"用户名", "UserName"})
@NotBlank(message = "用户名 不能为空")
private String userName;
}
此时导入的excel文件中,如果列名为 “用户名” ,则可以正常读取数据,而列名为 “UserName”,则读取的数据为null。
观察@ExcelProperty注解的定义可以发现 官方标注了只有第一个会生效(* read: When you have multiple heads, take the first one)。但在实际应用中,导入的excel文件的列可能随着业务场景有所区别,例如本文所示的多语言场景,导入文件中表头可能是中文,也可能是英文。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExcelProperty {
/**
* The name of the sheet header.
*
* <p>
* write: It automatically merges when you have more than one head
* <p>
* read: When you have multiple heads, take the first one
*
* @return The name of the sheet header
*/
String[] value() default {""};
2.解决方法
在处理导入文件时,需要自定义监听器如下:
重写invokeHeadMap方法,此方法在读取首行表头后会被调用,此时可以获取到excel文件的表头,因此循环遍历实体类的各个字段的@ExcelProperty注解的value值,若表头中包含该注解中的某个值,则表示文件中有匹配的列名,此时通过反射将@ExcelProperty注解的value值修改为这个列名,在后续easyExcel读取时就可以按照列名匹配到对应的列了!
public class MyExcelListener<T> extends AnalysisEventListener<T> {
private List<T> data = new ArrayList<>();
private Validator validator;
public Class<?> classType;
public PpcExcelListener(Class<?> classType) {
this.classType = classType;
// 这里用于实现对文件内容的校验,与本文无关
ValidatorFactory factory = Validation.byDefaultProvider()
.configure()
.messageInterpolator(new ParameterMessageInterpolator())
.buildValidatorFactory();
this.validator = factory.getValidator();
//
}
//用于实现实体类字段与多个列明的匹配,用于多语言模版的导入功能
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
// 读取表头后会调用此方法
// 按列名读取文件时,easyExcel默认ExcelProperty的value值只能匹配第一个,其他无效
changeColumnNameByReflect(headMap);
}
/**
* 通过反射修改实体类的ExcelProperty的value值,用于实现多语言的列名匹配
*/
private void changeColumnNameByReflect(Map<Integer, String> headMap) {
Collection<String> columnNames = headMap.values();
try {
Class<?> clazz = this.classType;
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
if (annotation != null) {
String[] valueList = annotation.value();
for (String value : valueList) {
if (columnNames.contains(value)) {
//获取 ExcelProperty 这个代理实例所持有的 InvocationHandler
InvocationHandler excelH = Proxy.getInvocationHandler(annotation);
// 获取 AnnotationInvocationHandler 的 memberValues 字段
Field excelF = excelH.getClass().getDeclaredField("memberValues");
// 因为这个字段是 private final 修饰,所以要打开权限
excelF.setAccessible(true);
// 获取 memberValues
Map excelValues = (Map) excelF.get(excelH);
// 修改 value 属性值
excelValues.put("value", new String[]{value});
}
}
}
}
} catch (Exception e) {
throw new RuntimeException("Excel列名转换失败");
}
}
@Override
public void invoke(T obj, AnalysisContext analysisContext) {
//每一行数据解析完成后都会调用这里
//参数校验代码,与本文内容无关-------------------
//最大行数限制
if (++currentRowNum > maxRowNum) {
throw new BssAppException(BssErrorCodeEnum.ERROR_BSS_IMPORT_EXCEED_MAX_ROWS, PlaceholderInfoBss.build(maxRowNum));
}
Set<ConstraintViolation<T>> violations = validator.validate(obj);
if (!violations.isEmpty()) {
// 收集所有校验错误的消息
for (ConstraintViolation<T> violation : violations) {
//根据参数校验类型匹配对应的 错误码
Class<? extends Annotation> annotationType = violation.getConstraintDescriptor().getAnnotation().annotationType();
ErrorCodeEnum ErrorCodeEnum = ErrorCodeEnum.getByValidAnnotation(annotationType);
//获取字段名作为key
//todo 获取自定义注解内容作为key
PathImpl propertyPath = (PathImpl) violation.getPropertyPath();
NodeImpl leafNode = propertyPath.getLeafNode();
String placeholderKey = leafNode.asString();
if (StrUtil.isNotBlank(placeholderKey)) {
//抛出异常
throw new AppException(ErrorCodeEnum,
PlaceholderInfo.build(currentRowNum, 1),
PlaceholderInfo.build(placeholderKey, 2));
}
}
}
//------------------------------------------------
data.add(obj);
}
}
然后在读取文件时,使用自定义的监听器即可:
public Response<Void> importMethod(MultipartFile file) throws IOException{
PpcExcelListener<MyDto > excelListener = new PpcExcelListener<(MyDto .class);
EasyExcel.read(file.getInputStream(), MyDto .class, excelListener)
.excelType(ExcelTypeEnum.XLSX)
.sheet()
.headRowNumber(1)//跳过表头
.doRead();
List<MyDto> dataList = excelListener.getData();
//.............
}
自此即可实现easyExcel对于列名的多值匹配,其本质是通过将实体类的字段的注解上的多个值与文件列名进行比对,只保留匹配一个列名。类似的,在文件导出时,也可以使用反射修改实体类的@ExcelProperty注解的value值,来实现动态的表头,此处不再赘述。