使用过EasyExcel的小朋友都知道,对于一张Excel表,如果我们想要将其解析好,然后导入数据库,后端需要封装对应的监听器去解析Excel,但是当Excel模板多了以后,就需要封装大量的监听器,而且监听里面有好多的冗余代码,需要我们自己去处理逻辑的,几乎就是存储数据到数据库部分。所以我们可不可以封装一个公共的监听器,然后编写对应监听继承此公共监听呢,这样我们只需要专心完善我们的入库逻辑即可。
EasyExcel官网:https://www.yuque.com/easyexcel/doc/easyexcel
需要注意的是,EasyExcel的监听器不能被Spring管理。
直接上公共监听器代码:
/**
* @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());
}
}
上面说了,应该在文件信息传入构造之前将其入库,那么在业务层可以这样写
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;
}
然后在控制器层可以这样写
@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
}
}
@Data
public class UploadParamDTO {
private Integer fileTag;
private String dataTime;
private InputStream inputStream;
private String fileName;
}
这里不用担心的是,EasyExcel在执行解析后会自动关闭输入流。
之前我们也说了,EasyExcel的监听器不能被Spring管理,也就是说,它里面的事务管理也是失效的。
那么我们如果入库过程中,出现了异常,我们应该采用何种操作呢?
第一种方式:
上面写的saveUploadFileInfo中不是存了文件标记么,我们可以在入库的数据中带上这个标记入库。
然后,如果我们解析过程中出现异常了,那么此文件解析肯定被标记为失败了,我们可以根据这个标记删除对应的数据。
也就是说,我们要执行 delete 操作,这确实有点极端。
第二种方式:
在监听器中通过构造注入事务管理相关类实现事务回滚。
/**
* @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());
}
}
在业务层注入事务管理类,然后通过监听器构造器传入即可。
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
ExcelUtil工具类
工具类导出部分,可以根据自己的需求定制,具体参照官方文档。
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);
}
}
如果根据上述操作出现了问题,可能是我哪一步遗漏了,在此我说声抱歉,请大家根据出现的问题自行百度排查。。
评论区