【easyExcel】读取excel时,实现列名的多值匹配

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值,来实现动态的表头,此处不再赘述。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值