赞
踩
用户导入数据, 将合理的数据入库, 未通过校验的数据, 告诉用户错误原因, 并下载给用户
部分数据导入成功, 失败的数据回显出错误原因(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 <T> 对应类型 */ 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 <T> 对应类型 */ 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); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。