目 录CONTENT

文章目录

EasyExcel抽离分装公共监听

筱晶哥哥
2022-03-25 / 0 评论 / 0 点赞 / 16 阅读 / 19302 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2024-03-23,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

使用过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);
    }
}

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

0

评论区