EasyExcel抽离分装公共监听

使用过EasyExcel的小朋友都知道,对于一张Excel表,如果我们想要将其解析好,然后导入数据库,后端需要封装对应的监听器去解析Excel,但是当Excel模板多了以后,就需要封装大量的监听器,而且监听里面有好多的冗余代码,需要我们自己去处理逻辑的,几乎就是存储数据到数据库部分。所以我们可不可以封装一个公共的监听器,然后编写对应监听继承此公共监听呢,这样我们只需要专心完善我们的入库逻辑即可。

EasyExcel官网:https://www.yuque.com/easyexcel/doc/easyexcel


需要注意的是,EasyExcel的监听器不能被Spring管理。

直接上公共监听器代码:

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
/**
* @author lijing
* @description 抽离封装公共数据监听
*/
public class CommonListener<T> extends AnalysisEventListener<T> {

/**
* 打印日志
*/
private static final Logger LOGGER = LoggerFactory.getLogger(CommonListener.class);

/**
* 每隔多少条存储数据库,实际使用中可以3000条,然后清理cachedDataList ,方便内存回收
*/
private static final int BATCH_COUNT = 1000;

/**
* 缓存List,待插入的数据,容量到了BATCH_COUNT后,则要入库,然后清空,往复,直至数据全部入库
*/
private List<T> cachedDataList = new ArrayList<>();

/**
* 数据业务类,这里面可以注入多个业务类,然后通过此属性调用,去操作数据库
*/
private BaseServices baseServices;

/**
* 文件信息
*/
private FileInfo fileInfo;

public List<T> getCachedDataList() {
return cachedDataList;
}

public BaseServices getBaseServices() {
return baseServices;
}

public FileInfo getFileInfo() {
return fileInfo;
}

/**
* 构造,上面也说了,EasyExcel的监听器不能被Spring管理,所以要通过构造传参
*/
public CommonListener(
BaseServices baseServices,
FileInfo fileInfo
) {
this.baseServices = baseServices;
this.fileInfo = fileInfo;
}

@Override
public void invoke(T data, AnalysisContext context) {
cachedDataList.add(data);
// 如果到了设定的批量值了,则执行入库操作,然后清空此次的缓存List
if (cachedDataList.size() >= BATCH_COUNT) {
try {
saveData();
} catch (Exception e) {
// 如果入库出现异常,则应该改变fileInfo中的状态字段,标记为失败
// 最好也录入失败原因
// TODO
LOGGER.error("数据解析异常!" + e.getMessage());
}
// 清空cachedDataList
cachedDataList.clear();
}
}

@Override
public void doAfterAllAnalysed(AnalysisContext context) {
try {
saveData();
// 改变fileInfo中的状态字段,标记为成功
// 最好将完成时间也录入,当然在更之前应该早早录入开始时间
// TODO
LOGGER.info("所有数据解析完成!");
} catch (Exception e) {
// 如果入库出现异常,则应该改变fileInfo中的状态字段,标记为失败
// 最好也录入失败原因
// TODO
LOGGER.error("数据解析异常!" + e.getMessage());
} finally {
// 执行文件修改操作,最好在FileInfo传入构造之前已经将文件信息存入库了,然后入库后自动返回其id
// TODO
}
}

/**
* 保存数据,入库操作
*/
public void saveData() {
LOGGER.info("{}条数据,开始存储数据库!", cachedDataList.size());
}
}

上面说了,应该在文件信息传入构造之前将其入库,那么在业务层可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private FileInfo saveUploadFileInfo(UploadParamDTO uploadParam) {
FileInfo fileInfo = new FileInfo();
// 上传文件名
fileInfo.setFileName(uploadParam.getFileName());
// 上传时间(yyyy-MM-dd HH:mm:ss)
fileInfo.setCreateTime("");
// 上传人
// TODO
// 上传状态,初始为上传中
// TODO
// 文件标记
fileInfo.setSign(""); // 这里的标记可以使用UUID
// 这里插入后是要自动填充主键的,需要在映射文件中加入:useGeneratedKeys="true" keyProperty="id"
fileInfoService.insertSelective(fileInfo);
return fileInfo;
}

然后在控制器层可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SneakyThrows
@PostMapping("/fileAnalysis")
@ApiOperation("解析excel文件入库")
public RestResult<?> fileAnalysis(MultipartFile file, Integer fileTag) {
UploadParamDTO uploadParam = new UploadParamDTO();
uploadParam.setInputStream(file.getInputStream());
uploadParam.setFileTag(fileTag);
uploadParam.setFileName(file.getOriginalFilename());
// fileTag调用业务层的具体的哪些文件解析
// 业务层的这些方法第一步就应该调用saveUploadFileInfo,将其文件初始信息保存后再执行解析
// 而且,文件的解析要做成异步的,因为有的文件数据量大,让它另起线程解析即可
switch (fileTag) {
// TODO
}
}
1
2
3
4
5
6
7
@Data
public class UploadParamDTO {
private Integer fileTag;
private String dataTime;
private InputStream inputStream;
private String fileName;
}

这里不用担心的是,EasyExcel在执行解析后会自动关闭输入流。

之前我们也说了,EasyExcel的监听器不能被Spring管理,也就是说,它里面的事务管理也是失效的。

那么我们如果入库过程中,出现了异常,我们应该采用何种操作呢?

第一种方式:

上面写的saveUploadFileInfo中不是存了文件标记么,我们可以在入库的数据中带上这个标记入库。

然后,如果我们解析过程中出现异常了,那么此文件解析肯定被标记为失败了,我们可以根据这个标记删除对应的数据。

也就是说,我们要执行 delete 操作,这确实有点极端。

第二种方式:

在监听器中通过构造注入事务管理相关类实现事务回滚。

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
/**
* @author lijing
* @description 抽离封装公共数据监听
*/
public class CommonListener<T> extends AnalysisEventListener<T> {

/**
* 事务管理
*/
private DataSourceTransactionManager dataSourceTransactionManager;
private DefaultTransactionDefinition transactionDefinition;
private TransactionStatus transactionStatus = null;

/**
* 打印日志
*/
private static final Logger LOGGER = LoggerFactory.getLogger(CommonListener.class);

/**
* 每隔多少条存储数据库,实际使用中可以3000条,然后清理cachedDataList ,方便内存回收
*/
private static final int BATCH_COUNT = 1000;

/**
* 缓存List,待插入的数据,容量到了BATCH_COUNT后,则要入库,然后清空,往复,直至数据全部入库
*/
private List<T> cachedDataList = new ArrayList<>();

/**
* 数据业务类,这里面可以注入多个业务类,然后通过此属性调用,去操作数据库
*/
private BaseServices baseServices;

/**
* 文件信息
*/
private FileInfo fileInfo;

public List<T> getCachedDataList() {
return cachedDataList;
}

public BaseServices getBaseServices() {
return baseServices;
}

public FileInfo getFileInfo() {
return fileInfo;
}

/**
* 构造,上面也说了,EasyExcel的监听器不能被Spring管理,所以要通过构造传参
*/
public CommonListener(
BaseServices baseServices,
FileInfo fileInfo
DataSourceTransactionManager dataSourceTransactionManager,
TransactionDefinition transactionDefinition
) {
this.baseServices = baseServices;
this.fileInfo = fileInfo;
this.dataSourceTransactionManager = dataSourceTransactionManager;
this.transactionDefinition = new DefaultTransactionDefinition(transactionDefinition);
// 设置事务的隔离级别 :未提交读写
this.transactionDefinition.setIsolationLevel(
TransactionDefinition.ISOLATION_READ_UNCOMMITTED
);
// 手动开启事务
this.transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
}

@Override
public void invoke(T data, AnalysisContext context) {

boolean hasCompleted = transactionStatus.isCompleted();
// 如果事务已经关闭
if (hasCompleted){
return;
}
cachedDataList.add(data);
// 如果到了设定的批量值了,则执行入库操作,然后清空此次的缓存List
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
// 清空cachedDataList
cachedDataList.clear();
}
}

@Override
public void doAfterAllAnalysed(AnalysisContext context) {
boolean hasCompleted = transactionStatus.isCompleted();
if (hasCompleted){
return;
}
saveData();
// 改变fileInfo中的状态字段,标记为成功
// 最好将完成时间也录入,当然在更之前应该早早录入开始时间
// TODO
LOGGER.info("所有数据解析完成!");

// 执行文件修改操作,最好在FileInfo传入构造之前已经将文件信息存入库了,然后入库后自动返回其id
// TODO

if (!hasCompleted){
// 提交事务
dataSourceTransactionManager.commit(transactionStatus);
log.info("Listener doAfterAllAnalysed:当前事务已提交");
}
}

@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
log.info("导入过程中出现异常会进入该方法,重写了父类方法");
log.info("结束前事务状态:" + transactionStatus.isCompleted());
dataSourceTransactionManager.rollback(transactionStatus);
log.info("结束后事务状态:" + transactionStatus.isCompleted());

// 修改文件状态为失败
// TODO

throw exception;
}

/**
* 保存数据,入库操作
*/
public void saveData() {
LOGGER.info("{}条数据,开始存储数据库!", cachedDataList.size());
}
}

在业务层注入事务管理类,然后通过监听器构造器传入即可。

1
2
3
4
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;

ExcelUtil工具类

工具类导出部分,可以根据自己的需求定制,具体参照官方文档。

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
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.util.List;

@Slf4j
public class ExcelUtil {

/**
* 导出
* @param response 响应
* @param excelName Excel名称
* @param sheetName sheet页名称
* @param clazz Excel要转换的类型
* @param data 要导出的数据
* @throws Exception
*/
public static void export2Web(HttpServletResponse response, String excelName, String sheetName, Class clazz, List data) throws Exception {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// URLEncoder.encode可以防止中文乱码
excelName = URLEncoder.encode(excelName, "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + excelName + ExcelTypeEnum.XLSX.getValue());
EasyExcel.write(response.getOutputStream(), clazz).sheet(sheetName).doWrite(data);
}
}

如果根据上述操作出现了问题,可能是我哪一步遗漏了,在此我说声抱歉,请大家根据出现的问题自行百度排查。。

点击查看

本文标题:EasyExcel抽离分装公共监听

文章作者:LiJing

发布时间:2022年03月25日 - 14:34:50

最后更新:2023年06月03日 - 10:01:15

原始链接:https://blog-next.xiaojingge.com/posts/2096914293.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------------本文结束 感谢您的阅读-------------------