老版本 EasyExcel 一个神出鬼没的异常 - 教程
2025-10-13 15:25 tlnshuju 阅读(0) 评论(0) 收藏 举报1 背景
数据中台上线以来,总有一个 同步作业
偶尔会抛出异常:com.alibaba.excel.exception.ExcelAnalysisException: Converter not found, convert STRING to java.lang.String
作业通过 datax
同步用户在 ftp 上传的文件数据到数据中台;datax
会调用使用 EasyExcel 库加载 Excel 文件的数据,并在加载过程中偶尔抛出异常。
异常是由于无法找到,把 Excel 中的 字符串 Cell
转换为 java 字符串
的 转换器,这就很奇怪,这应该是一个最基本的转换器了。
这个 神出鬼没 的异常,有以下几个特点:
- 只有固定的
同步作业
会抛出异常,其余的同步作业都运行正常; - 这个异常是偶发的,每个月会发生 4-5 次;
- 作业由于异常运行失败后 5 分钟,再重试,就成功了,非常诡异。
虽然,重试机制可以保证作业最终是运行成功的,但,只要抛出这个异常,整条数据链路的完成时间就会延迟。因此,必须找出导致异常的 root cause。
2 根本原因分析
2.1 EasyExcel 的源码分析
首先,数据中台当前使用的 EasyExcel 版本是 2.1.4
,已经是 2019 年的老版本了:
(1) 分析异常栈
异常由 ConverterUtils
的 130 行抛出,具体代码如下:
由于无法从 converterMap
中获取转换器,从而抛出 ExcelDataConvertException
异常。
converterMap
里面保存了 Excel Cell 数据类型 - java 类型
与 转换器
的对应关系。
因此需要分析 converterMap
是如何被初始化的,有可能在运行到 ConverterUtils
的 130 行 时,converterMap
仍未完成初始化,导致无法获取 转换器
。
(2) 分析
converterMap
如何初始化
converterMap
来自AbstractHolder
类:converterMap
的初始化时机
在读取 Excel 数据时,会实例化类 ReadSheetHolder
,它是 AbstractHolder
的子类(ReadSheetHolder
-继承-> AbstractReadHolder
-继承-> AbstractHolder
)
在实例化 ReadSheetHolder
时,会调用 AbstractReadHolder
的构造器:AbstractReadHolder
的构造器会调用 DefaultConverterLoader.loadDefaultReadConverter()
对 converterMap
进行赋值。
(3)
DefaultConverterLoader
初始化转换器
的Map
- 首先,
loadDefaultReadConverter
是类级别的静态方法; loadDefaultReadConverter
最终会调用loadAllConverter
方法;loadAllConverter
会判断静态属性allConverter
是否为null
,如果非null
则直接返回allConverter
,否则初始化allConverter
中的转换器。
(4) 异常根本原因分析
- 首先,作业是要同步文件夹中的 4 个文件到数据中台,并发度为 3,即同时会有 3 个线程同步文件,因此,
loadAllConverter
运行在一个多线程的场景下; loadAllConverter
方法没有做线程安全处理;- 会出现一种情况,线程 A 执行到
allConverter = new HashMap<String, Converter>(64);
B线程刚好执行到
if (allConverter != null) {
return allConverter;
}
就返回了空的 allConverter
了,由此,导致无法找到 转换器
,而抛出 ExcelDataConvertException
异常。
这种由于没有做线程安全处理而导致的异常,也符合它神出鬼没的特性。
2.2 本地复现异常
2.2.1 先在 maven 引入数据中台版本的 EasyExcel 依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.4</version>
</dependency>
2.2.2 编写测试代码
package com.nutanix.test;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
/**
* 使用EasyExcel读取Excel文件并转换为Map集合的工具类
*/
public class ExcelToMapReader {
/**
* 读取Excel文件并转换为List<Map<String, Object>>* @param filePath Excel文件路径* @return 包含Excel数据的Map集合列表,每个Map对应一行数据*/public static List<Map<String, Object>> readExcelToMap(String filePath) {// 创建一个监听器实例ExcelMapListener listener = new ExcelMapListener();// 读取Excel文件EasyExcel.read(filePath, listener).sheet() // 读取第一个sheet.doRead(); // 执行读取操作// 返回读取到的数据return listener.getDataList();}/*** 自定义监听器,用于处理Excel读取事件*/private static class ExcelMapListener extends AnalysisEventListener<Map<String, Object>> {// 存储读取到的数据private List<Map<String, Object>> dataList = new ArrayList<>();/*** 每读取一行数据都会调用此方法* @param data 一行数据,键是表头,值是单元格内容* @param context 分析上下文*/@Overridepublic void invoke(Map<String, Object> data, AnalysisContext context) {dataList.add(data);}/*** 读取完成后调用此方法* @param context 分析上下文*/@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 可以在这里添加读取完成后的处理逻辑System.out.println("Excel文件读取完成,共读取 " + dataList.size() + " 行数据");}/*** 获取读取到的数据列表* @return 数据列表*/public List<Map<String, Object>> getDataList() {return dataList;}}public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {readExcelToMap("/tmp/customer-file1.xlsx");});Thread thread2 = new Thread(() -> {readExcelToMap("/tmp/customer-file2.xlsx");});Thread thread3 = new Thread(() -> {readExcelToMap("/tmp/customer-file3.xlsx");});Thread thread4 = new Thread(() -> {readExcelToMap("/tmp/customer-file4.xlsx");});thread.start();thread2.start();thread3.start();thread4.start();}}
程序模拟数据中台的作业,启动 4 个线程,分别读取对应的 excel 文件。
2.2.3 打断点
在 DefaultConverterLoader
的 loadAllConverter()
打断点:
2.2.4 调试
- 运行程序,4 个线程都会在断点处停下;
- 选择其中 1 个线程,并 Step Over 到
allConverter = new HashMap<String, Converter>(64);
后,例如下图; - 再选择另一个线程,并让它恢复运行;
- 异常重现
3 新版本的 EasyExcel 处理
在新版本的 EasyExcel,阿里团队已经通过采用静态代码块的方式,在 DefaultConverterLoader
首次被访问时,对 allConverter
进行了初始化,从而解决了在并发环境的线程安全问题:
4 总结
至此,根因已经找到了,就是因为,老版本的 EasyExcel,在初始化 类型与转换器的 Map 对象时,没有做线程安全处理,导致,在并发环境下,ExcelDataConvertException
异常神出鬼没地出现。
希望本文,对其他遇到这个问题的同学有帮助。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/936193.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!