easyExcel通用导入工具类

lijunyi2022-09-09javaexcel

需求前景

近期在做几个关于excel文件上传导入数据库的操作,于是便在Easyexcel的基础上造了个轮子。

主要涉及的技术有

  • 反射
  • @FunctionalInterface 函数式接口

轮子开始

函数式接口:ThrowingConsumer

import java.util.function.Consumer;

/**
 * @version 1.0.0
 * @className: ThrowingConsumer
 * @description: 因为自带的Consumer不会抛出异常,这里我们重写了一个可以抛出异常的Consumer
 * @author: LiJunYi
 * @create: 2022/9/9 13:58
 */
@FunctionalInterface
public interface ThrowingConsumer<T> extends Consumer<T>
{
    /**
     * 重写accept方法,捕获并抛出自定义业务异常
     *
     * @param t t
     */
    @Override
    default void accept(T t) {
        try {
            acceptBase(t);
        } catch (Exception ex) {
            throw new RRException(ex.getMessage());
        }
    }

    /**
     * 对给定参数执行消费操作
     *
     * @param t t
     */
    void acceptBase(T t);
}

EasyExcelImportUtil

/**
 * @version 1.0.0
 * @className: EasyExcelImportUtil
 * @description: easyExcel通用导入解析
 * @author: LiJunYi
 * @create: 2022/9/9 13:55
 */
public class EasyExcelImportUtil<T>
{
    /**
     * 使用自定义监听器
     *
     * @param listener    自定义监听器
     * @param fileStream  文件流
     * @param elementType 元素类型
     */
    public static <T> void importFile(InputStream fileStream, Class<T> elementType, ReadListener<T> listener) {
        EasyExcel.read(fileStream, elementType, listener).sheet().doRead();
    }

    /**
     * 通用导入excel文件方法
     *
     * @param fileStream 导入的文件流
     * @param elementType 接收excel每行数据的实体类型
     * @param serviceAction 数据解析完成后调用的业务逻辑方法
     * @param <T> 实体类型
     */
    public static <T> void importFile(InputStream fileStream, Class<?> elementType, ThrowingConsumer<List<T>> serviceAction) {
        // 获取excel通用监听器
        EasyExcelImportCommonListener<T> commonListener = new EasyExcelImportCommonListener<>(serviceAction);
        // 读取excel文件并导入
        EasyExcel.read(fileStream,elementType, commonListener).sheet().doRead();
    }

    /**
     * 通用导入excel文件方法
     *
     * @param fileStream 导入的文件流
     * @param elementType 接收excel每行数据的实体类型
     * @param serviceAction 数据解析完成后调用的业务逻辑方法
     * @param parameter   其他参数
     * @param field       字段名称
     */
    public static <T> void importFile(InputStream fileStream, Class<?> elementType, ThrowingConsumer<List<T>> serviceAction, Map<String, ?> parameter, String field) {
        // 获取excel通用监听器
        EasyExcelImportCommonListener<T> commonListener = new EasyExcelImportCommonListener<>(serviceAction,parameter,field);
        // 读取excel文件并导入
        EasyExcel.read(fileStream,elementType, commonListener).sheet().doRead();
    }
}

EasyExcelImportCommonListener

/**
 * @version 1.0.0
 * @className: EasyExcelImportCommonListener
 * @description: EasyExcel通用导入时监听处理器
 * @author: LiJunYi
 * @create: 2022/9/9 13:56
 */
@Slf4j
public class EasyExcelImportCommonListener<T> implements ReadListener<T>
{
    /**
     * 每隔 200 条存储数据库,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 200;

    /**
     * 转换后插入数据表的实体
     */
    private List<T> persistentDataList = Lists.newArrayList();

    /**
     * 具体业务逻辑接口方法
     */
    private final ThrowingConsumer<List<T>> persistentActionMethod;
    
    /**
     * 需要设置对应字段时的参数及数值缓存
     */
    private Map<String, ?> parameter = null;

    /**
     * 字段名,同时也是 parameter 的 key
     */
    private String field = null;

    /**
     * 构造函数注入:不包括其他参数
     *
     * @param persistentActionMethod 具体业务逻辑接口方法
     */
    public EasyExcelImportCommonListener(ThrowingConsumer<List<T>> persistentActionMethod) {
        this.persistentActionMethod = persistentActionMethod;
    }

    /**
     * 构造函数(包含其他参数)
     *
     * @param persistentActionMethod 持续操作方法
     * @param parameter              参数
     * @param field                  需要通过反射设置值的字段名
     */
    public EasyExcelImportCommonListener(ThrowingConsumer<List<T>> persistentActionMethod, Map<String, ?> parameter, String field) {
        this.persistentActionMethod = persistentActionMethod;
        this.parameter = parameter;
        this.field = field;
    }

    /**
     * 在转换异常 获取其他异常情况下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
     *
     * @param exception 异常
     * @param context   上下文
     * @throws Exception 异常
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        if (exception instanceof ExcelDataConvertException) {
            ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
            log.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(),
                    excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData().getStringValue());
        }
    }

    /**
     * 返回每个sheet页的表头
     *
     * @param headMap 头Map集合
     * @param context 上下文
     */
    @Override
    public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
        Map<Integer, String> headMapping = ConverterUtils.convertToStringMap(headMap, context);
        if (CollUtil.isEmpty(headMapping))
        {
            // 抛出自定义业务异常转全局处理
            throw new RRException("表头不允许为空!");
        }
    }

    /**
     * 每一条数据解析都会来调用
     *
     * @param t               t
     * @param analysisContext 分析上下文
     */
    @Override
    public void invoke(T t, AnalysisContext analysisContext)
    {
        if (ObjectUtil.isNotNull(field))
        {
            try {
                // 获取指定的字段
                Field relationIdField = t.getClass().getDeclaredField(field);
                relationIdField.setAccessible(Boolean.TRUE);
                // 通过参数获取需要设置的值
                Object v = parameter.get(field);
                relationIdField.set(t, v);
            }catch (Exception e)
            {
                log.error("excel解析时,通过反射设置对应字段值异常,原因:{}", e.getMessage());
            }
        }
        // 校验导入字段:校验字段同样是通过反射进行实现的
        // 可以参考博客中《利用反射比较并记录对象修改的值》这篇文章
        // ExcelImportValid.validRequireField(t);
        // 存入集合缓存
        persistentDataList.add(t);
        // 当数据达到最大插入数量后则进行入库操作,防止大数量情况下OOM
        if (persistentDataList.size() >= BATCH_COUNT) {
            // 进行业务数据插入
            this.insertDataToDb(persistentDataList);
            // 清空集合
            persistentDataList.clear();
        }
    }


    /**
     * 所有数据解析完后,回调
     *
     * @param analysisContext 分析上下文
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        // 在此处调用入库操作,防止有剩余数据未入库
        if (CollUtil.isNotEmpty(persistentDataList))
        {
            insertDataToDb(persistentDataList);
        }
    }

    /**
     * 插入数据到数据库
     *
     * @param data 数据
     */
    private void insertDataToDb(List<T> data) {
        // 对数据分组,批量插入
        List<List<T>> dataList = ListUtil.split(data, BATCH_COUNT);
        // 调用业务方法进行后续操作
        dataList.forEach(persistentActionMethod);
    }
}
Last Updated 2024/5/24 16:21:58