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);
}
实现效果
可根据需求进行更改