EasyExcel验证器,批注表格错误信息并导出

EasyExcel验证数据验证器,批注表格错误信息并导出

基本流程

1 、基于EasyExcel的 AnalysisEventListener<Map<Integer, String>> 实现验证器(抽象类,需要按照具体需求实现验证功能)

2、每个单元格匹配校验,检验失败存入Redis 下载时将错误数据解析并导出到Excel表格

依赖导入


<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.3.2</version>
</dependency>
<!-- 使用了HuTool工具类 -->
<dependency>
 	<groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.21</version>
</dependency>

<!-- 其他相关依赖 如Redis -->

验证器实现

import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;

import java.util.*;

/**
 * Excel 验证工具
 *
 * @author ycy
 * @since 2023/10/31
 */
public abstract class ExcelVerify extends AnalysisEventListener<Map<Integer, String>> {

    /**
     * Excel 错误映射<br>
     * key为行数,value为具体错误信息
     */
    protected Map<Integer, List<ExcelError>> excelErrorMap = new HashMap<>();

    /**
     * excel表头信息<br>
     * key 为列下标
     */
    private Map<Integer, String> heard;

    /**
     * 每行数据列表
     */
    private final List<Map<Integer, String>> dataList = new ArrayList<>();

    /**
     * 行指针
     */
    private int rowPointer = 0;

    public List<Map<Integer, String>> getDataList() {
        return dataList;
    }

    public Map<Integer, List<ExcelError>> getExcelErrorMap() {
        return excelErrorMap;
    }


    public Map<Integer, String> getHeard() {
        return heard;
    }

    /**
     * 设置错误批注集合
     *
     * @param rowsNum   行索引
     * @param cellIndex 单元格索引
     * @param msg       错误信息
     */
    protected void setExcelErrorMaps(int rowsNum, int cellIndex, String msg) {
        if (excelErrorMap.containsKey(rowsNum)) {
            List<ExcelError> excelErrors = excelErrorMap.get(rowsNum);
            excelErrors.add(new ExcelError(rowsNum, cellIndex, msg));
            excelErrorMap.put(rowsNum, excelErrors);
        } else {
            List<ExcelError> excelErrors = new ArrayList<>();
            excelErrors.add(new ExcelError(rowsNum, cellIndex, msg));
            excelErrorMap.put(rowsNum, excelErrors);
        }
    }


    /**
     * All listeners receive this method when any one Listener does an error report. If an exception is thrown here, the
     * entire read will terminate.
     *
     * @param exception
     * @param context
     * @throws Exception
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        throw exception;
    }

    /**
     * 每行读取出发函数
     *
     * @param data    行数据key 为列索引
     * @param context 上下文
     */
    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {
        //将表头与数据融合构造
        Map<String, String> rowData = new HashMap<>();
        //根据表头格式化行数据
        //解决问题 当表格出现某个列有空值为填写时,可能会读取不到该列
        Map<Integer, String> indexData = new HashMap<>();
        heard.keySet().forEach(index -> {
            rowData.put(heard.get(index), data.get(index));
            indexData.put(index, data.get(index));
        });


        //遍历数据进行验证
        indexData.forEach((colIndex, value) -> {
            try {
                //验证并获取格式化后的值
                String formatValue = verifyAndFormatColumn(rowPointer, colIndex, heard.get(colIndex), value, rowData);

                //若格式化后的值不为null添加格式化后的值
                if (formatValue != null) {
                    indexData.put(colIndex, formatValue);
                }

            } catch (ExcelVerifyException e) {
                //设置一条错误信息
                setExcelErrorMaps(rowPointer, colIndex, e.getMessage());

                //可直接返回错误不添加信息,即关闭批注功能
//                throw new ServiceException("{}行{}列`{}`验证错误:{}", rowPointer + 2, ExcelUtil.indexToColName(colIndex), heard.get(colIndex), e.getMessage());
            }
        });
        //添加已读取内容
        dataList.add(indexData);

        //行指针
        rowPointer++;
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
    }

    /**
     * Returns the header as a map.Override the current method to receive header data.
     *
     * @param headMap
     * @param context
     */
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        super.invokeHeadMap(headMap, context);
        //在解析头信息时 保存头信息
        this.heard = headMap;
        verifyHead(headMap);
    }


    /**
     * 获取实体列表<br>
     * 根据读取到的数据和实体类的 @ExcelProperty 注解解析构造实体类对象
     *
     * @param entityClass 实体类对象
     * @return {@link List}<{@link T}>
     */
    public <T> List<T> getEntityList(Class<T> entityClass) {
        //表格验证有错误不进行实体类转化
        //有校验错误直接返回 null
        if (ObjUtil.isNotEmpty(excelErrorMap)) {
            return null;
        }

        //列名 字段名对应关系
        Map<String, String> columnFieldNameMap = new HashMap<>();
        Arrays.stream(ReflectUtil.getFields(entityClass)).forEach(field -> {
            //判断是否被 @ExcelProperty 修饰
            if (field.isAnnotationPresent(ExcelProperty.class)) {
                //获取其列名数组
                String[] columnNames = field.getAnnotation(ExcelProperty.class).value();
                for (String columnName : columnNames) {
                    columnFieldNameMap.put(columnName, field.getName());
                }
            }
        });

        //列名 下标 对应关系
        Map<String, Integer> columnIndexMap = MapUtil.inverse(heard);

        //下标 字段名 对应关系
        Map<Integer, String> indexFieldMap = new HashMap<>();
        columnFieldNameMap.keySet().forEach(columnName -> {
            indexFieldMap.put(columnIndexMap.get(columnName), columnFieldNameMap.get(columnName));
        });

        List<T> entityList = new ArrayList<>();

        dataList.forEach(dataMap -> {
            HashMap<String, String> fieldValueMap = new HashMap<>();
            dataMap.keySet().forEach(index -> {
                fieldValueMap.put(indexFieldMap.get(index), dataMap.get(index));
            });
            entityList.add(Convert.convert(entityClass, fieldValueMap));
        });


        return entityList;

    }


    /**
     * 验证EXCEL表格 格式化内容<br>
     * <p>
     * 验证列是否符合要求<br>
     * 若返回值不为null则更改读取的内容<br>
     * 调用时输入列名和列数据, 若出现错误则抛出错误{@link  ExcelVerifyException}
     *
     * @param rowIndex   行索引
     * @param colIndex   列索引
     * @param columnName 列名称
     * @param value      数据
     * @param row        该行数据(key为表头列名)
     * @return {@link String}
     * @throws ExcelVerifyException 验证错误返回该异常会在invoke方法中捕捉并添加错误信息
     */
    protected abstract String verifyAndFormatColumn(Integer rowIndex, Integer colIndex, String columnName, String value, Map<String, String> row) throws ExcelVerifyException;


    /**
     * 验证表头,表头错误没有读取的必要直接抛出错误
     *
     * @param heard 表头,key为表头索引,value为表头内容
     */
    protected abstract void verifyHead(Map<Integer, String> heard);


    /**
     * Excel 验证异常
     *
     * @author ycy
     * @since 2023/10/31
     */
    public static class ExcelVerifyException extends RuntimeException {


        public ExcelVerifyException() {
            super("");
        }

        public ExcelVerifyException(String message) {
            super(message);
        }


        /**
         * 字符串模板构造<br/>
         * 封装HuTool{@link StrUtil#format(CharSequence, Object...)}<br>
         *
         * 可调用 new ExcelVerifyException("I'm error msg: {}", "info") 和log使用方法一致
         *
         */
        public ExcelVerifyException(CharSequence template, Object... params) {
            super(StrUtil.format(template, params));
        }
    }


}

错误信息记录实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExcelError implements Serializable {

    /**
     * 行
     */
    private int row;

    /**
     * 列
     */
    private int column;

    /**
     * 错误信息
     */
    private String msg;


}

批注导出功能


import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;

import java.util.List;

/**
 * 将参数校验失败的Exccel,添加批注后导出
 *
 * @author ycy
 * @since 2023/10/31
 */
public class CommentWriteHandler extends AbstractRowWriteHandlerAdapter {

    @Override
    public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
        if (!isHead) {
            Sheet sheet = writeSheetHolder.getSheet();
            if (excelErrorMap.containsKey(relativeRowIndex)) {
                List<ExcelError> excelErrors = excelErrorMap.get(relativeRowIndex);
                excelErrors.forEach(obj -> {
                    setCellCommon(sheet, obj.getRow() + 1, obj.getColumn(), obj.getMsg());
                });
            }
        }
    }
}
import com.alibaba.excel.write.handler.AbstractRowWriteHandler;
import org.apache.poi.ss.usermodel.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * AbstractRowWriteHandler适配器
 *
 * @author ycy
 * @since 2023/10/31
 */
public abstract class AbstractRowWriteHandlerAdapter extends AbstractRowWriteHandler {
    protected Map<Integer, List<ExcelError>> excelErrorMap = new HashMap<>();

    public void setExcelErrorMap(Map<Integer, List<ExcelError>> excelErrorMap) {
        this.excelErrorMap = excelErrorMap;
    }

    /**
     * 设置单元格批注
     *
     * @param sheet    sheet
     * @param rowIndex 行索引
     * @param colIndex 列索引
     * @param value    批注
     */
    protected void setCellCommon(Sheet sheet, int rowIndex, int colIndex, String value) {
        Workbook workbook = sheet.getWorkbook();
        CellStyle cellStyle = workbook.createCellStyle();
        cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        cellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        Row row = sheet.getRow(rowIndex);
        if (row == null) {
            return;
        }
        Cell cell = row.getCell(colIndex);
        if (cell == null) {
            cell = row.createCell(colIndex);
        }
        if (value == null) {
            cell.removeCellComment();
            return;
        }
        Drawing<?> drawing = sheet.createDrawingPatriarch();
        CreationHelper factory = sheet.getWorkbook().getCreationHelper();
        ClientAnchor anchor = factory.createClientAnchor();
        Row row1 = sheet.getRow(anchor.getRow1());
        if (row1 != null) {
            Cell cell1 = row1.getCell(anchor.getCol1());
            if (cell1 != null) {
                cell1.removeCellComment();
            }
        }
        Comment comment = drawing.createCellComment(anchor);
        RichTextString str = factory.createRichTextString(value);
        comment.setString(str);
        comment.setAuthor("admin");
        cell.setCellComment(comment);
        cell.setCellStyle(cellStyle);
    }
}

ExcelTool

封装工具类便于使用

package com.lingen.utils;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ReflectUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.annotation.ExcelProperty;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Excel 工具
 *
 * @author ycy
 * @date 2023/10/31
 */
@Component
public class ExcelTool {


    /**
     * Redis 缓存工具
     */
    @Resource
    RedisCache redisCache;


    /**
     * 从Excel中验证表格并获取表中内容<br>
     * 若Excel未通过验证则将表格信息(带错误批注)存储到 Redis 时间5分钟
     *
     * @param entityClass 实体类
     * @param inputStream 输入流
     * @param validator   校验器
     * @return {@link List}<{@link T}> 校验通过读取到的内容
     * @throws IOException    ioexception
     * @throws ExcelException 若校验未通过,抛出该异常该异常字段 uuid 为内容缓存标识
     */
    public static  <T> List<T> getDataList(Class<T> entityClass, InputStream inputStream, ExcelVerify validator) throws IOException {
        //执行读取验证
        EasyExcel.read(inputStream, validator).sheet().doRead();

        //是否存在错误信息
        if (ObjUtil.isEmpty(validator.getExcelErrorMap())) {
            return validator.getEntityList(entityClass);
        } else {
            //存在生成缓存key 并缓存验证器(包含表格信息)
            String simpleUid = IdUtil.fastSimpleUUID();
            //将带有信息的验证器存入Redis
            redisCache.setCacheObject(RedisKey.EXCEL_CACHE_KEY + simpleUid, validator, 5, TimeUnit.MINUTES);
            //自定义异常 带上错误UUID
            throw new ExcelException(simpleUid, "读取错误, 错误标识: {}", simpleUid);
        }
    }

    /**
     * 下载批注后到错误Excel
     *
     * @param uuid Redis缓存key
     * @throws Exception 例外
     */
    public void downloadErrorInfo(String uuid) throws Exception {
        //获取返回对象 写入基本信息
        HttpServletResponse response = ServletUtils.getResponse();
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        // 这里URLEncoder.encode可以防止中文乱码
        String excelName = URLEncoder.encode("错误批注-" + DateUtil.format(new Date(), "yy-MM-dd-HH"), "UTF-8");
        response.setHeader("Content-disposition", "attachment;filename=" + excelName + ExcelTypeEnum.XLSX.getValue());

        //获取表格信息(验证器对象)
        ExcelVerify validator = redisCache.getCacheObject(RedisKey.EXCEL_CACHE_KEY + uuid);

        //批注处理器
        CommentWriteHandler commentWriteHandler = new CommentWriteHandler();
        commentWriteHandler.setExcelErrorMap(validator.getExcelErrorMap());

        //写出到流
        //这里直接写出到返回流中,若需要可将流从参数传入
        EasyExcel.write(response.getOutputStream())
                .head(validator.getHeard().values().stream().map(CollUtil::toList).collect(Collectors.toList()))
                .inMemory(Boolean.TRUE)
                .sheet("sheet1")
                //注册批注拦截器
                .registerWriteHandler(commentWriteHandler)
                .doWrite(validator.getDataList());

    }


    /**
     * 根据实体类的@ExcelProperty 注解校验表头<br>
     * 验证是否有缺失列或者冗余列
     *
     * @param entityClass 实体类
     * @param inputHeader 需要校验的表头列名集合
     * @throws ServiceException 检验未通过抛出异常
     */
    public static void checkHeaderByEntity(Class<?> entityClass, Collection<String> inputHeader) throws ServiceException {
        //对输入的表头进行重复性校验
        if (inputHeader.size() != CollUtil.distinct(inputHeader).size()) {
            throw new ServiceException("包含重复列名");
        }
        //获取实体类中规定的字段
        List<String[]> specifiedHeader = Arrays.stream(ReflectUtil.getFields(entityClass, field -> field.isAnnotationPresent(ExcelProperty.class)))
                .map(field -> field.getAnnotation(ExcelProperty.class).value()).collect(Collectors.toList());


        // 定义一个缺失列表
        List<String> missing = new ArrayList<>();
        // 定义一个重复列表
        List<String> duplicate = new ArrayList<>();
        // 遍历实体类中ExcelProperty规定的表头标题
        for (String[] arr : specifiedHeader) {
            // 计数变量,用于记录在inputHeader中找到了多少个包含该数组的元素
            int count = 0;
            for (String s : arr) {
                // 如果inputHeader中包含该元素,就将 count 加一
                if (inputHeader.contains(s)) {
                    count++;
                }
            }
            // 如果 count 为零,表示没有在输入的列名中缺少该列, 将ExcelProperty定义的列名添加到缺失列表
            if (count == 0) {
                missing.add(arr[0]);
            }
            // 如果 count 大于一,表示在输入列中有多列对应实体类一个字段
            if (count > 1) {
                for (String s : arr) {
                    if (inputHeader.contains(s)) {
                        duplicate.add(s);
                    }
                }
            }
        }
        if (ObjUtil.isNotEmpty(missing) || ObjUtil.isNotEmpty(duplicate)) {
        	//构造错误信息
            String missingMsg = ObjUtil.isNotEmpty(missing) ? "表格缺失所需的(" + CollUtil.join(missing, ",") + ")列" : "";
            String duplicateMsg = ObjUtil.isNotEmpty(duplicate) ? "表格(" + CollUtil.join(duplicate, ",") + ")列对应同个个数据,请确认需要的数据项" : "";
            throw new ServiceException(missingMsg + duplicateMsg);
        }

    }

}


异常类 便于使用捕捉异常 和获取错误ID

public class ExcelException extends RuntimeException {

    private String uuid;

    public ExcelException() {
        super("");
    }

    public ExcelException(String uuid, String message) {
        super(message);
        this.uuid = uuid;
    }


    /**
     * 字符串模板构造<br/>
     * 封装{@link StrUtil#format(CharSequence, Object...)}
     */
    public ExcelException(String uuid, CharSequence template, Object... params) {
        super(StrUtil.format(template, params));
        this.uuid = uuid;
    }
}
	

具体使用

按需求实现验证器

public class DemoVerify extends ExcelVerify {
	
	@Override
    protected String verifyAndFormatColumn(Integer rowIndex, Integer colIndex, String columnName, String value, Map<String, String> row) throws ExcelVerifyException {
    	//实现具体单元格验证逻辑
       
        if???{
            //验证错误返回该错误会记录进excelErrorMap
            throw new ExcelVerifyException()
        }
        
        
        //若出现 
        // a b c d
        // 1 2 3 4
        // 1 2 3 
        // 1 2 3 
        // 1 2 3 
        // q w e r
        // q w e
        // q w e
        //要将d列作为以下所有的值
        //可通过rowIndex colIndex 在datalist往前遍历获取并返回值
        
        //当无需格式化时,返回null将使用读取到的值
        //当返回值时,读取会替换该单元格的值
        return null;
        
    
    }
    
    @Override
    protected void verifyHead(Map<Integer, String> heard) {
        //实现验证表头逻辑
        //若无特殊需求可直接使用工具类提供的验证
        ExcelTool.checkHeaderByEntity(Demo.class, heard.values());
    }
}

controller层示例

    @PostMapping("/upload")
    public R<Void> upload(MultipartFile file) throws IOException {
        try {
        	//传入实体类对象,输入流以及实现的对应的验证器
        	List<Demo> entityList = ExcelTool.getDataList(Demo.class, file.getInputStream(), new DemoVerify());
        	//拿到数据进行处理
            
        	return R.ok();
        } catch (ExcelException e) {
            return R.fail(e.getUuid());
        }
    }


    @GetMapping("/err/{uuid}")
    public void downloadErrorInfo(@PathVariable String uuid) throws Exception {
        excelTool.downloadErrorInfo(uuid);
    }

实现效果

在这里插入图片描述

可根据需求进行更改

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
使用EasyExcel可以轻松地导出表格并进行下载。首先,您需要在您的项目中添加EasyExcel的依赖。然后,您可以创建一个导出对象,如引用\[2\]中的BasicSysUserExcelData类。在该类中,您可以使用@ExcelProperty注解来指定每个字段在Excel中的列名。接下来,您可以使用EasyExcel提供的工具类来进行导出操作。以下是一个简单的示例代码: ```java // 创建导出数据列表 List<BasicSysUserExcelData> dataList = new ArrayList<>(); // 添加数据到列表中 // 设置导出的文件路径和文件名 String filePath = "path/to/exported/file.xlsx"; // 执行导出操作 EasyExcel.write(filePath, BasicSysUserExcelData.class).sheet("Sheet1").doWrite(dataList); ``` 在上述代码中,您需要将数据添加到dataList中,并指定导出的文件路径和文件名。然后,使用EasyExcel.write()方法指定导出的文件和数据类型,并使用.sheet()方法指定导出的Sheet名称。最后,使用.doWrite()方法执行导出操作。 完成导出后,您可以将生成的Excel文件提供给用户进行下载。用户可以通过点击下载链接或使用其他方式下载该文件。 请注意,上述代码只是一个简单的示例,您可能需要根据您的具体需求进行适当的修改和扩展。 #### 引用[.reference_title] - *1* *2* [EasyExcel导出excel表格](https://blog.csdn.net/LF15527554599/article/details/123925722)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值