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 分析上下文
*/
@Override
public void invoke(Map<String, Object> data, AnalysisContext context) {
dataList.add(data);
}
/**
* 读取完成后调用此方法
* @param context 分析上下文
*/
@Override
public 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 异常神出鬼没地出现。
希望本文,对其他遇到这个问题的同学有帮助。

1846

被折叠的 条评论
为什么被折叠?



