EasyExcel导入, 失败时回填错误原因, 成功则正常返回

需求背景

用户导入数据, 将合理的数据入库, 未通过校验的数据, 告诉用户错误原因, 并下载给用户

查看效果

部分数据导入成功, 失败的数据回显出错误原因(Excel中自动调整的行高列宽)

其中{{stuNum}}为正常测试数据, 非bug

EasyExcel导入, 失败时回填错误原因, 成功则正常返回_第1张图片

环境介绍

兼容性需要自己考虑

  • easyexcel3.3.2
  • springboot3.0.1
  • jdk17

使用方式

@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);
    }
}

你可能感兴趣的:(springboot,excel,java)