用户导入数据, 将合理的数据入库, 未通过校验的数据, 告诉用户错误原因, 并下载给用户
部分数据导入成功, 失败的数据回显出错误原因(Excel中自动调整的行高列宽)
其中{{stuNum}}
为正常测试数据, 非bug
兼容性需要自己考虑
@PostMapping("import")
public Res<List<TestStudentImportDTO>> readExcel(MultipartFile file) {
ExcelUtil.read(file, TestStudentImportDTO.class, t -> {
// 模拟service中业务异常
if (!t.getStuNum().startsWith("No.")) {
throw new LyException.Normal("学号必须以\"No.\"开头");
}
});
return Res.ok("已全部导入");
}
附: TestStudentImportDTO.java
package kim.nzxy.ly.modules.test.dto;
import com.alibaba.excel.annotation.ExcelProperty;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDate;
/**
* @author ly.chn
*/
@Data
public class TestStudentImportDTO {
/**
* 学号
*/
@ExcelProperty("学号")
@NotEmpty(message = "学号不能为空")
private String stuNum;
/**
* 学号
*/
@ExcelProperty("学生姓名")
@NotEmpty(message = "学生姓名不能为空")
private String stuName;
/**
* 性别, 字典: sex
*/
@ExcelProperty("性别")
@NotBlank(message = "性别不能为空")
private String sex;
/**
* 生日
*/
@ExcelProperty("生日")
@NotNull(message = "请填写生日")
private LocalDate birthday;
/**
* 受表扬次数
*/
@ExcelProperty("受表扬次数")
@NotNull(message = "受表扬次数, 如果为空, 请填写0")
private Integer starCount;
}
结构
关键内容
ExcelContextUtil: 用于设置下载Excel时处理文件名
ExcelErrorFillHandler: 用于填充错误信息
ExcelImportListener: 用于解析Excel内容, 收集解析数据
ExcelLineResult: 存储Excel解析后的结构
ExcelUtil: Excel工具类
其余通用工具类等
- LyException: 自定义系统业务异常
- RequestContextUtil: 用于静态方法获取request/response
ExcelUtil.java
package kim.nzxy.ly.common.config.excel;
import com.alibaba.excel.EasyExcel;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import kim.nzxy.ly.common.exception.LyException;
import kim.nzxy.ly.common.util.RequestContextUtil;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* excel工具
*
* @author ly-chn
*/
@Slf4j
public class ExcelUtil {
/**
* 导入, 标题行默认为1
*
* @param file 文件
* @param pojoClass 实体类
* @param consumer 消费数据, 执行SQL逻辑或其他逻辑等等,
* 如果抛出LyException异常, 则异常message将作为Excel导入失败原因
* 否则为未知异常导致导入失败
* @param 对应类型
*/
public static <T> void read(@NotNull MultipartFile file, @NotNull Class<T> pojoClass, @NotNull Consumer<T> consumer) {
read(file, pojoClass, consumer, 1);
}
/**
* 导入
*
* @param file 文件
* @param pojoClass 实体类
* @param consumer 消费数据, 执行SQL逻辑或其他逻辑等等,
* 如果抛出LyException异常, 则异常message将作为Excel导入失败原因
* 否则为未知异常导致导入失败
* @param titleLineNumber 标题所在行, 从1开始
* @param 对应类型
*/
public static <T> void read(@NotNull MultipartFile file,
@NotNull Class<T> pojoClass,
@NotNull Consumer<T> consumer,
@NotNull Integer titleLineNumber) {
try {
ExcelImportListener<T> listener = new ExcelImportListener<>(consumer);
@Cleanup InputStream inputStream = file.getInputStream();
EasyExcel.read(inputStream, pojoClass, listener)
.headRowNumber(titleLineNumber)
.sheet().doRead();
List<ExcelLineResult<T>> resultList = listener.getExcelLineResultList();
boolean allSuccess = resultList.stream()
.allMatch(it -> it.getViolation().isEmpty() && Objects.isNull(it.getBizError()));
if (allSuccess) {
log.info("Excel 数据已全部导入: {}", resultList);
return;
}
log.error("Excel校验失败, 读取结果: {}", resultList);
HttpServletResponse response = RequestContextUtil.getResponse();
@Cleanup InputStream templateIs = file.getInputStream();
ExcelContextUtil.setDownloadHeader(response, "文件导入失败.xlsx");
EasyExcel.write(response.getOutputStream(), pojoClass)
.withTemplate(templateIs)
.autoCloseStream(false)
.registerWriteHandler(new ExcelErrorFillHandler<T>(resultList, titleLineNumber))
.needHead(false)
.sheet()
.doWrite(Collections.emptyList());
} catch (Exception e) {
log.error("文件读取失败", e);
throw new LyException.Normal("文件读取失败, 请检查文件格式");
}
throw new LyException.None();
}
}
ExcelContextUtil.java
package kim.nzxy.ly.common.config.excel;
import jakarta.servlet.http.HttpServletResponse;
import kim.nzxy.ly.common.exception.LyException;
import kim.nzxy.ly.common.util.RequestContextUtil;
import org.apache.poi.openxml4j.opc.internal.ContentType;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* excel导入导出过程中用到的工具类
*
* @author xuyingfa
*/
public class ExcelContextUtil {
private static final String SUFFIX = ".xlsx";
/**
* 为下载文件设置响应头
*
* @param response 响应
* @param filename 文件名
*/
public static void setDownloadHeader(HttpServletResponse response, String filename) {
if (!filename.endsWith(SUFFIX)) {
filename += SUFFIX;
}
response.setCharacterEncoding("utf-8");
filename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("\\+", "%20");
// axios下载时获取文件名
response.setHeader("filename",filename);
response.setHeader("Content-Disposition", "attachment; filename=" + filename);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
}
}
ExcelErrorFillHandler.java
package kim.nzxy.ly.common.config.excel;
import com.alibaba.excel.write.handler.RowWriteHandler;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.handler.context.SheetWriteHandlerContext;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import kim.nzxy.ly.common.util.LyValidationUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author ly-chn
*/
@Slf4j
@RequiredArgsConstructor
public class ExcelErrorFillHandler<T> implements SheetWriteHandler, RowWriteHandler {
/**
* 错误结果集
*/
private final List<ExcelLineResult<T>> resultList;
/**
* 标题所在行, 从1开始
*/
private final Integer titleLineNumber;
/**
* 结果列序号
*/
private int resultColNum;
/**
* 默认导入成功的提示
*/
private static final String SUCCESS_MSG = "导入成功";
private static void setCellStyle(Cell cell, IndexedColors color) {
Workbook workbook = cell.getSheet().getWorkbook();
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setColor(color.getIndex());
style.setFont(font);
cell.setCellStyle(style);
}
@Override
public void afterSheetCreate(SheetWriteHandlerContext context) {
SheetWriteHandler.super.afterSheetCreate(context);
}
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
Sheet cachedSheet = writeSheetHolder.getCachedSheet();
for (int i = 1; i <= cachedSheet.getLastRowNum() + 1; i++) {
// 空白数据, 不做处理
if (i < titleLineNumber) {
continue;
}
Row row = cachedSheet.getRow(i - 1);
// 标题行, 创建标题
if (i == titleLineNumber) {
this.resultColNum = row.getLastCellNum();
Cell cell = row.createCell(row.getLastCellNum(), CellType.STRING);
setCellStyle(cell, IndexedColors.BLACK);
cell.setCellValue("导入结果");
continue;
}
// 结果行
Cell cell = row.createCell(this.resultColNum, CellType.STRING);
String errMsg = convertErrMsg(resultList.get(i - titleLineNumber - 1));
if (errMsg == null) {
setCellStyle(cell, IndexedColors.GREEN);
cell.setCellValue(SUCCESS_MSG);
continue;
}
setCellStyle(cell, IndexedColors.RED);
cell.setCellValue(errMsg);
}
}
/**
* 解析每行的错误信息
*
* @param result 读取结果
* @return 错误信息
*/
private String convertErrMsg(ExcelLineResult<T> result) {
if (result.getBizError() != null) {
return result.getBizError();
}
if (result.getViolation().isEmpty()) {
return null;
}
return result.getViolation().stream().map(LyValidationUtil::getMessage)
.collect(Collectors.joining(";\n"));
}
}
ExcelLineResult.java
package kim.nzxy.ly.common.config.excel;
import jakarta.validation.ConstraintViolation;
import lombok.Builder;
import lombok.Data;
import java.util.Set;
/**
* Excel按行导入结果
*
* @author ly-chn
*/
@Data
@Builder
class ExcelLineResult<T> {
/**
* 行号, 从0开始
*/
private Integer rowIndex;
/**
* 导入的数据
*/
private T target;
/**
* 校验结果
*/
private Set<ConstraintViolation<T>> violation;
/**
* 业务异常错误信息
*/
private String bizError;
}
ExcelImportListener.java
package kim.nzxy.ly.common.config.excel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import kim.nzxy.ly.common.exception.LyException;
import kim.nzxy.ly.common.util.SpringContextUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
/**
* excel导入校验
*
* @author ly-chn
*/
@Slf4j
@RequiredArgsConstructor
class ExcelImportListener<T> implements ReadListener<T> {
private final List<ExcelLineResult<T>> excelLineResultList = new ArrayList<>();
public static String defaultBizError = "未知异常";
/**
* 业务处理, 入库, 解析等
*/
private final Consumer<T> consumer;
/**
* 每次读取, 记录读取信息
*/
@Override
public void invoke(T t, AnalysisContext analysisContext) {
if (log.isDebugEnabled()) {
log.debug("读取到数据: {}", t);
}
ExcelLineResult<T> build = ExcelLineResult.<T>builder()
.rowIndex(analysisContext.readRowHolder().getRowIndex())
.target(t)
.build();
excelLineResultList.add(build);
}
/**
* 读取完毕后执行校验
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
if (excelLineResultList.isEmpty()) {
return;
}
Validator validator = SpringContextUtil.getBean(Validator.class);
excelLineResultList.forEach(it -> {
Set<ConstraintViolation<T>> validate = validator.validate(it.getTarget());
it.setViolation(validate);
// 校验不通过, 不必执行业务逻辑
if (!validate.isEmpty()) {
return;
}
try {
consumer.accept(it.getTarget());
} catch (LyException e) {
log.error("解析数据失败: {}, 异常信息: {}", it, e.getMessage());
it.setBizError(e.getMessage());
} catch (Exception e) {
log.error("解析数据失败", e);
it.setBizError(defaultBizError);
}
});
}
public List<ExcelLineResult<T>> getExcelLineResultList() {
return excelLineResultList;
}
}
LyException.java
完整代码见github
package kim.nzxy.ly.common.exception;
import lombok.Getter;
/**
* 全局异常
*
* @author ly-chn
*/
public class LyException extends RuntimeException {
public LyException(String message) {
super(message);
}
}
RequestContextUtil.java
package kim.nzxy.ly.common.util;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import kim.nzxy.ly.common.exception.LyException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Optional;
/**
* spring上下文相关工具类
*
* @author ly-chn
*/
@Slf4j
public class RequestContextUtil {
/**
* @return 获取当前请求
*/
public static HttpServletRequest getRequest() {
return getRequestAttributes().getRequest();
}
/**
* @return 获取当前响应
*/
public static HttpServletResponse getResponse() {
return getRequestAttributes().getResponse();
}
private static ServletRequestAttributes getRequestAttributes() {
RequestAttributes attributes = Optional.ofNullable(RequestContextHolder.getRequestAttributes()).orElseThrow(() -> {
log.error("非web上下文无法获取请求属性, 异步操作请在同步操作内获取所需信息");
return new LyException.Panic("请求异常");
});
return ((ServletRequestAttributes) attributes);
}
}