easyExcel通用导入工具类

9/9/2022 excel

# 需求前景

近期在做几个关于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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 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();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153