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

该文章介绍了如何在JavaSpringBoot应用中利用EasyExcel处理用户上传的Excel数据,对数据进行校验,未通过校验的数据会返回错误原因,并能下载包含错误信息的Excel文件。主要涉及的类包括ExcelUtil、ExcelErrorFillHandler和ExcelImportListener,以及自定义的异常处理类LyException。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

需求背景

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

查看效果

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

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

image-20230720165738176

环境介绍

兼容性需要自己考虑

  • 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 <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);
    }
}
### EasyExcel 导入常见错误及其解决方案 #### 1. 数据读取失败 当使用 `easyexcel` 进行数据导入,如果遇到无法读取数据的情况,通常是因为监听器配置不当或文件路径不正确。确保使用的工具类如 `ExcelUtil`, `ExcelListener`, 和其他辅助类已经按照官方文档进行了正确实现[^1]。 对于此类问题的一个典型处理方式是在初始化读取操作前验证输入流的有效性和可访问性: ```java InputStream inputStream = new FileInputStream(new File(filePath)); EasyExcel.read(inputStream, YourDataClass.class, new ExcelListener()).sheet().doRead(); ``` #### 2. Linux环境下字体缺失引发的异常 在Linux服务器环境中运行程序并调用 `easyexcel` 的某些功能(特别是涉及图表渲染等功能)可能会因为缺少必要的字体资源而抛出异常。具体表现为与 `FontConfiguration` 或者 `fontconfig` 相关的日志报错信息[^2]。 针对这个问题可以通过安装DejaVu Sans Fonts来解决,在Dockerfile或其他部署脚本中加入如下命令以确保所需字体被正确加载: ```bash RUN yum install -y dejavu-sans-fonts fontconfig ``` 此外还需要确认Java应用程序能够找到这些新安装的字体库位置;有可能需要重启服务使更改生效。 #### 3. 处理大数据量导致内存溢出 另一个常见的问题是处理大规模Excel文件容易触发OOM (Out Of Memory),即超出JVM堆空间限制。为了避免这种情况发生,建议采用分页读取的方式逐步解析工作表内的记录而不是一次性全部载入内存之中。 通过调整参数设置可以有效缓解此现象带来的影响: ```properties # application.properties 配置项示例 spring.boot.easyexcel.max-read-line=10000 ``` 或者编程层面控制每次只获取固定数量的数据行再做后续逻辑运算。
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值