Java如何基于EasyExcel實(shí)現(xiàn)導(dǎo)入數(shù)據(jù)校驗(yàn)并生成錯(cuò)誤信息Excel
功能設(shè)計(jì)
由于項(xiàng)目中涉及到大量的文件導(dǎo)入功能,故考慮設(shè)計(jì)一個(gè)excel導(dǎo)入的通用框架,解決以下問(wèn)題
- 導(dǎo)入的數(shù)據(jù)不可信任,可能出現(xiàn)空值校驗(yàn)的許多判斷,如果將這些判斷加入業(yè)務(wù)代碼可能會(huì)造成大量代碼的堆積,如下情況:
if(name==null){
throw new RuntimeException("名稱(chēng)不能為空");
}
if(age==null){
throw new RuntimeException("年齡不能為空");
}
if(sex==null){
throw new RuntimeException("性別不能為空");
}
if(order.size()>10){
throw new RuntimeException("訂單號(hào)長(zhǎng)度不能大于10");
}
EasyExcel幫我處理導(dǎo)入文件時(shí),只是簡(jiǎn)單的根據(jù)列名把內(nèi)容set到字段上,如果字段類(lèi)型不符是會(huì)
直接報(bào)錯(cuò)的!而我們需要將數(shù)據(jù)的錯(cuò)誤內(nèi)容提交給用戶(hù),所以如下的報(bào)錯(cuò)是不可取的
針對(duì)文件中的問(wèn)題,需要清晰地呈現(xiàn)給用戶(hù),每一行具體出現(xiàn)了哪種類(lèi)型的錯(cuò)誤,例如如下:

基于EasyExcel封裝,由于項(xiàng)目中本身使用的EasyExcel,考慮到不改動(dòng)項(xiàng)目的技術(shù)組成,還是基于EasyExcel開(kāi)發(fā)。
設(shè)計(jì)思路
EasyExcel做的工作其實(shí)很簡(jiǎn)單,就是把文件中的內(nèi)容映射到我們實(shí)體類(lèi)的字段上,我們要做的就是在映射前和映射后做校驗(yàn)

代碼解析
我先把完整代碼貼上,下面再詳細(xì)分析
注解類(lèi)
/**
* 導(dǎo)入校驗(yàn)注解
*
* @author wangmeng
* @since 2024/5/25
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelCheck {
/**
* 是否可以為空,默認(rèn)是
*/
boolean canEmpty() default true;
/**
* 是否可以重復(fù),默認(rèn)是
*/
boolean canRepeat() default true;
/**
* 長(zhǎng)度校驗(yàn),只對(duì)String生效
*/
int length() default -1;
}
錯(cuò)誤信息實(shí)體類(lèi)
/**
* excel導(dǎo)入錯(cuò)誤信息
*
* @author wangmeng
* @since 2024/5/25
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class ExcelErrorMessage {
/**
* 行號(hào)
*/
private Integer rowNum;
/**
* 列名
*/
private String colHeaderName;
/**
* 錯(cuò)誤信息
*/
private String message;
}
導(dǎo)入通用的listener
/**
* excel導(dǎo)入共通監(jiān)聽(tīng)類(lèi)
*
* @author wangmeng
* @since 2024/5/25
*/
@Slf4j
public class CheckableImportListener<T> extends AnalysisEventListener<T> {
/**
* check注解對(duì)象
*/
protected List<Object[]> filedList;
/**
* excel數(shù)據(jù)
*/
protected final List<T> list = new ArrayList<>();
/**
* 錯(cuò)誤信息集合
*/
@Getter
private final List<ExcelErrorMessage> errorList = new ArrayList<>();
private Boolean isEmpty = false;
public CheckableImportListener() {
super();
}
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
log.error("解析單元格失敗,", exception);
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
log.error("第{}行,第{}列解析異常,數(shù)據(jù)為:{}", excelDataConvertException.getRowIndex(),
excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData());
}
}
@Override
public void invoke(T data, AnalysisContext context) {
if (CollectionUtils.isEmpty(list)) {
Class<?> clazz = data.getClass();
//含check注解的字段
filedList = Arrays.stream(clazz.getDeclaredFields())
.filter(o -> null != o.getAnnotation(ExcelCheck.class))
.map(o -> new Object[]{o, o.getAnnotation(ExcelCheck.class), o.getAnnotation(ExcelProperty.class)}).collect(Collectors.toList());
}
log.info("data:{}", JSON.toJSONString(data));
list.add(data);
if (CollectionUtils.isNotEmpty(filedList)) {
checkEmpty(data);
//存在空值則不進(jìn)行其他校驗(yàn)
if (isEmpty) {
return;
}
// 校驗(yàn)長(zhǎng)度
checkLength(data);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if (isEmpty) {
return;
}
errorList.sort(Comparator.comparing(ExcelErrorMessage::getRowNum));
}
/**
* 檢驗(yàn)非空
*
* @param data
*/
public void checkEmpty(T data) {
for (Object[] os : filedList) {
Field filed = (Field) os[0];
filed.setAccessible(true);
ExcelCheck excelCheck = (ExcelCheck) os[1];
ExcelProperty excelProperty = (ExcelProperty) os[2];
try {
//校驗(yàn)非空
if (!excelCheck.canEmpty()) {
if (filed.get(data) == null ||
(filed.getType() == String.class && StringUtils.isEmpty((String) filed.get(data)))) {
errorList.add(new ExcelErrorMessage()
.setRowNum(list.size() + 1)
.setColHeaderName(excelProperty.value()[0])
.setMessage(excelProperty.value()[0] + "字段不能為空!"));
isEmpty = true;
}
}
} catch (IllegalAccessException e) {
log.error("校驗(yàn)excel信息失敗,", e);
e.printStackTrace();
}
}
}
/**
* 校驗(yàn)長(zhǎng)度
*
* @param data
*/
public void checkLength(T data) {
for (Object[] os : filedList) {
Field filed = (Field) os[0];
filed.setAccessible(true);
ExcelCheck excelCheck = (ExcelCheck) os[1];
ExcelProperty excelProperty = (ExcelProperty) os[2];
try {
//校驗(yàn)非空
if (excelCheck.length() > 0 && filed.getType() == String.class) {
String value = (String) filed.get(data);
if (value.length() > excelCheck.length()) {
errorList.add(new ExcelErrorMessage()
.setRowNum(list.size() + 1)
.setColHeaderName(excelProperty.value()[0])
.setMessage(excelProperty.value()[0] + "字段長(zhǎng)度大于" + excelCheck.length() + "!"));
}
}
} catch (IllegalAccessException e) {
log.error("校驗(yàn)字段長(zhǎng)度失敗,", e);
throw new RuntimeException(e);
}
}
}
/**
* 檢驗(yàn)重復(fù)
*/
public void checkRepeat() {
List<Object[]> repeatAnnotation = filedList.stream().filter(o -> {
ExcelCheck excelCheck = (ExcelCheck) o[1];
return !excelCheck.canRepeat();
}).collect(Collectors.toList());
for (Object[] objects : repeatAnnotation) {
ExcelProperty property = (ExcelProperty) objects[2];
//使用iterate方式構(gòu)建流以獲取行號(hào)
Stream.iterate(0, i -> i + 1).limit(list.size()).collect(Collectors.groupingBy(i -> {
Field field = (Field) objects[0];
String result = "";
try {
field.setAccessible(true);
result = JSON.toJSONString(field.get(list.get(i)));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return result;
}, LinkedHashMap::new, Collectors.mapping(i -> i + 2, Collectors.toList())))
.forEach((k, v) -> {
if (v.size() > 1) {
for (int i = 0; i < v.size(); i++) {
if (i == 0) {
continue;
}
errorList.add(new ExcelErrorMessage()
.setRowNum(v.get(i))
.setColHeaderName(property.value()[0])
.setMessage(property.value()[0] + "字段重復(fù)!"));
}
}
});
}
}
public void addError(Integer index, String errorMessage) {
ExcelErrorMessage excelErrorMessage = new ExcelErrorMessage().setRowNum(index).setMessage(errorMessage);
errorList.add(excelErrorMessage);
}
}
導(dǎo)入處理器類(lèi)
/**
* excel導(dǎo)入處理器,在easyExcel基礎(chǔ)封裝,增加通用讀取、校驗(yàn)功能
*
* @author wangmeng
* @since 2024/6/7
*/
@Setter
@Getter
@Accessors(chain = true)
@Slf4j
public class ExcelImportProcessor {
/**
* 默認(rèn)校驗(yàn)類(lèi)型listener
*/
private CheckableImportListener<?> listener = new CheckableImportListener<>();
private Consumer<ExcelReaderBuilder> readerBuilderConsumer;
/**
* 默認(rèn)第一個(gè)sheet
*/
private Integer sheetNo = 0;
/**
* 錯(cuò)誤列名
*/
private final static String ERROR_COLUMN_NAME = "錯(cuò)誤信息";
public ExcelImportProcessor() {
}
public ExcelImportProcessor(CheckableImportListener<?> listener) {
this.listener = listener;
}
public <R> List<R> importData(MultipartFile file, Class<R> clazz) {
// 校驗(yàn)文件
validateExcel(file);
List<R> dataList = null;
try (InputStream inputStream = file.getInputStream()) {
ExcelReaderBuilder readerBuilder = EasyExcel.read(inputStream, clazz, listener);
if (readerBuilderConsumer != null) {
readerBuilderConsumer.accept(readerBuilder);
}
dataList = readerBuilder.sheet(sheetNo).doReadSync();
} catch (ExcelAnalysisException e) {
ExcelDataConvertException exception = (ExcelDataConvertException) e.getCause();
List<ExcelErrorMessage> errorList = listener.getErrorList();
String headerName = exception.getExcelContentProperty().getField().getAnnotation(ExcelProperty.class).value()[0];
errorList.add(new ExcelErrorMessage().setRowNum(exception.getRowIndex() + 1)
.setColHeaderName(headerName)
.setMessage("'" + headerName + "'類(lèi)型轉(zhuǎn)換失敗,請(qǐng)輸入正確格式"));
} catch (IOException ioe) {
log.info("導(dǎo)入失敗,異常,", ioe);
throw new RuntimeException("導(dǎo)入失敗!");
}
if (CollectionUtils.isEmpty(dataList)) {
throw new RuntimeException("解析數(shù)據(jù)為空!");
}
return dataList;
}
public List<ExcelErrorMessage> getErrorList() {
return listener.getErrorList();
}
/**
* 手動(dòng)添加錯(cuò)誤
*
* @param index data的下標(biāo)(從0開(kāi)始)
* @param errorMessage 錯(cuò)誤信息
*/
public void addError(Integer index, String errorMessage) {
// 下標(biāo)從0開(kāi)始+1,標(biāo)題占一行+1,總計(jì)+2
Integer row = index + 2;
listener.addError(row, errorMessage);
}
/**
* 生成錯(cuò)誤信息excel,在原excel文件追加錯(cuò)誤列
*
* @param filePath 源文件路徑
*/
public Boolean generateErrorSheet(String filePath) {
List<ExcelErrorMessage> errorList = listener.getErrorList();
if (CollectionUtils.isEmpty(errorList)) {
return false;
}
Map<Integer, String> errorMap = errorList.stream().collect(Collectors.groupingBy(ExcelErrorMessage::getRowNum,
Collectors.mapping(ExcelErrorMessage::getMessage, Collectors.joining(";"))));
Workbook workbook = null;
// 打開(kāi)原excel文件
try (
FileInputStream inputStream = new FileInputStream(filePath)) {
workbook = new XSSFWorkbook(inputStream);
Sheet sheet = workbook.getSheetAt(sheetNo);
// 添加錯(cuò)誤列
Row headerRow = sheet.getRow(0);
short lastCellNum = headerRow.getLastCellNum();
// 檢查是否已經(jīng)存在錯(cuò)誤列
Cell lastValidCell = headerRow.getCell(lastCellNum - 1);
if (lastValidCell != null) {
if (!ERROR_COLUMN_NAME.equals(lastValidCell.getStringCellValue())) {
Cell errorHeaderCell = headerRow.createCell(lastCellNum);
errorHeaderCell.setCellValue(ERROR_COLUMN_NAME);
errorMap.forEach((rowNum, msg) -> {
Row row = sheet.getRow(rowNum - 1);
if (row != null) {
Cell errorCell = row.createCell(lastCellNum);
errorCell.setCellValue(msg);
}
});
} else {
int lastRowNum = sheet.getLastRowNum();
for (int rowNum = 1; rowNum <= lastRowNum; rowNum++) {
Row row = sheet.getRow(rowNum);
String setErrorMsg = errorMap.get(rowNum + 1);
// 如果沒(méi)有需要設(shè)置的錯(cuò)誤信息,要把舊的錯(cuò)誤信息清除
Cell errorCell = row.getCell(lastCellNum - 1);
if (setErrorMsg == null) {
if (errorCell != null) {
errorCell.setCellValue((String) null);
}
} else {
if (errorCell == null) {
errorCell = row.createCell(lastCellNum - 1);
}
errorCell.setCellValue(setErrorMsg);
}
}
}
}
} catch (IOException e) {
log.error("生成錯(cuò)誤信息失敗,", e);
throw new RuntimeException("生成錯(cuò)誤信息失敗");
}
try (FileOutputStream outputStream = new FileOutputStream(filePath)) {
// 寫(xiě)回去
workbook.write(outputStream);
workbook.close();
} catch (IOException e) {
log.error("生成錯(cuò)誤信息失敗,", e);
throw new RuntimeException("生成錯(cuò)誤信息失敗");
}
return true;
}
public static boolean isExcel2007(String filePath) {
return filePath.matches("^.+\\.(?i)(xlsx)$");
}
/**
* 驗(yàn)證EXCEL文件
*
* @param file
* @return
*/
public static void validateExcel(MultipartFile file) {
if (file == null) {
throw new RuntimeException("文件為空!");
}
String fileName = file.getOriginalFilename();
if (fileName != null && !isExcel2007(fileName)) {
throw new RuntimeException("導(dǎo)入文件必須是xlsx格式!");
}
if (StringUtils.isEmpty(fileName) || file.getSize() == 0) {
throw new RuntimeException("文件內(nèi)容不能為空");
}
}
}
捕獲類(lèi)型轉(zhuǎn)換異常
導(dǎo)入的第一步就是處理字段類(lèi)型錯(cuò)誤,因?yàn)槿绻霈F(xiàn)類(lèi)型轉(zhuǎn)換錯(cuò)誤,會(huì)直接導(dǎo)致程序異常,這里通過(guò)try,catch捕獲ExcelAnalysisException異常來(lái)獲取出現(xiàn)錯(cuò)誤的列和行。

這里通過(guò)exception對(duì)象獲取到了field,再獲取字段上的ExcelProperty注解。
在AnalysisEventListener中實(shí)現(xiàn)校驗(yàn)邏輯
在listener中的invoke方法中為每一行數(shù)據(jù)做校驗(yàn),這里主要使用了反射

獲取到Error后,根據(jù)錯(cuò)誤信息生成Excel
這里是拿導(dǎo)入的原本Excel文件,在最后追加一列錯(cuò)誤信息列,并將錯(cuò)誤信息與行對(duì)應(yīng),代碼如下

總結(jié)
到此這篇關(guān)于Java如何基于EasyExcel實(shí)現(xiàn)導(dǎo)入數(shù)據(jù)校驗(yàn)并生成錯(cuò)誤信息Excel的文章就介紹到這了,更多相關(guān)EasyExcel導(dǎo)入數(shù)據(jù)校驗(yàn)生成錯(cuò)誤信息內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java獲取鍵盤(pán)輸入的數(shù)字,并進(jìn)行排序的方法
今天小編就為大家分享一篇java獲取鍵盤(pán)輸入的數(shù)字,并進(jìn)行排序的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-07-07
Linux環(huán)境卸載Centos7自帶的OpenJDK和安裝JDK1.8圖文教程
CentOS系統(tǒng)是開(kāi)發(fā)者常用的Linux操作系統(tǒng),安裝它時(shí)會(huì)默認(rèn)安裝自帶的舊版本的OpenJDK,但在開(kāi)發(fā)者平時(shí)開(kāi)發(fā)Java項(xiàng)目時(shí)還是需要完整的JDK,這篇文章主要給大家介紹了關(guān)于Linux環(huán)境卸載Centos7自帶的OpenJDK和安裝JDK1.8的相關(guān)資料,需要的朋友可以參考下2024-07-07
JAVA實(shí)現(xiàn)掃描線算法(超詳細(xì))
掃描線算法就是從Ymin開(kāi)始掃描,然后構(gòu)建出NET,之后根據(jù)NET建立AET。接下來(lái)本文通過(guò)代碼給大家介紹JAVA實(shí)現(xiàn)掃描線算法,感興趣的朋友一起看看吧2019-10-10
java打印表格 將ResultSet中的數(shù)據(jù)打印成表格問(wèn)題
這篇文章主要介紹了java打印表格 將ResultSet中的數(shù)據(jù)打印成表格問(wèn)題。具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12
淺談maven的jar包和war包區(qū)別 以及打包方法
下面小編就為大家分享一篇淺談maven的jar包和war包區(qū)別 以及打包方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助2017-11-11

