[异常]EasyExcel问题之Lombok的Accessors注解与CGLib BeanMap冲突
背景
easyExcel作为阿里系的开源excel读写库,使用起来非常方别,而且对于大文件的读写对内存的占用也很少,曾经使用Apache poi写入千万级行记录的数据poi很容易内存溢出,改用easyExcel就没有遇到过。而且使用easyExcel读取千万级行记录的文件占内存占用也不大。至此之后,除了特殊场景,基本都是使用easyExcel。
问题
最近在使用EasyExcel时,遇到读取到的行记录的属性都为null问题。最终一番排查发现时Lombok的Accessors注解与CGLib BeanMap冲突问题导致。
解决方法
去除Excel模型的Lombok注解Accessors。
分析过程
easyExcel读取文件代码
@Test
public void testReadFile() {
//内存做一份excel文件用于演示
ByteArrayOutputStream os = new ByteArrayOutputStream();
TestModel testModel = new TestModel();
testModel.setName("张三");
EasyExcel.write(os, TestModel.class).sheet().doWrite(Arrays.asList(testModel));
//读取excel
ByteArrayInputStream inputStream = new ByteArrayInputStream(os.toByteArray());
SyncReadListener syncReadListener = new SyncReadListener();
EasyExcel.read(inputStream, TestModel.class, syncReadListener).sheet().doRead();
List<Object> list = syncReadListener.getList();
//校验
Assertions.assertThat(list).hasSize(1);
Assertions.assertThat(list.get(0)).isNotNull();
Assertions.assertThat(((TestModel)list.get(0)).getName()).isEqualTo("张三");
}
@Data
@Accessors(chain = true)
public static class TestModel{
@ExcelProperty("名字")
private String name;
}
输出结果如下:
Expecting:
<null>
to be equal to:
<"张三">
but was not.
Expected :张三
Actual :null
上述代码按照我们的预期应该是单测通过的,但是很奇怪的是居然结果异常,读取到的模型属性不是“张三”而是null
easyExcel使用cglib调用链
@startuml
'https://plantuml.com/sequence-diagram
autonumber
Tests -> ExcelReaderSheetBuilder ++ : doRead
ExcelReaderSheetBuilder -> ExcelReader ++ : read
ExcelReader -> ExcelAnalyser ++ : analysis
ExcelAnalyser -> XlsxSaxAnalyser ++ : execute
XlsxSaxAnalyser -> JAXPSAXParser ++ : parse
JAXPSAXParser -> XML11Configuration ++ : parse
XML11Configuration -> XMLDocumentFragmentScannerImpl ++ : scanDocument
XMLDocumentFragmentScannerImpl -> AbstractSAXParser ++ : endElement
AbstractSAXParser -> XlsxRowHandler ++ : endElement
XlsxRowHandler -> RowTagHandler ++ : endElement
RowTagHandler -> DefaultAnalysisEventProcessor ++ : endRow
DefaultAnalysisEventProcessor -> ModelBuildEventListener ++ : invoke
ModelBuildEventListener -> BeanMap ++ : putAll
@enduml
通过上述调用链,最后找到以下处理逻辑ModelBuildEventListener.buildUserModel方法
private Object buildUserModel(Map<Integer, CellData> cellDataMap, ReadHolder currentReadHolder,
AnalysisContext context) {
ExcelReadHeadProperty excelReadHeadProperty = currentReadHolder.excelReadHeadProperty();
Object resultModel;
try {
resultModel = excelReadHeadProperty.getHeadClazz().newInstance();
} catch (Exception e) {
throw new ExcelDataConvertException(context.readRowHolder().getRowIndex(), 0,
new CellData(CellDataTypeEnum.EMPTY), null,
"Can not instance class: " + excelReadHeadProperty.getHeadClazz().getName(), e);
}
Map<Integer, Head> headMap = excelReadHeadProperty.getHeadMap();
Map<String, Object> map = new HashMap<String, Object>(headMap.size() * 4 / 3 + 1);
Map<Integer, ExcelContentProperty> contentPropertyMap = excelReadHeadProperty.getContentPropertyMap();
for (Map.Entry<Integer, Head> entry : headMap.entrySet()) {
Integer index = entry.getKey();
if (!cellDataMap.containsKey(index)) {
continue;
}
CellData cellData = cellDataMap.get(index);
if (cellData.getType() == CellDataTypeEnum.EMPTY) {
continue;
}
ExcelContentProperty excelContentProperty = contentPropertyMap.get(index);
Object value = ConverterUtils.convertToJavaObject(cellData, excelContentProperty.getField(),
excelContentProperty, currentReadHolder.converterMap(), currentReadHolder.globalConfiguration(),
context.readRowHolder().getRowIndex(), index);
if (value != null) {
map.put(excelContentProperty.getField().getName(), value);
}
}
BeanMap.create(resultModel).putAll(map);
return resultModel;
}
重点如下:
Object resultModel = excelReadHeadProperty.getHeadClazz().newInstance();
BeanMap.create(resultModel).putAll(map);
其功能是对EasyExcel读取到数据的Map对象转换成用户给到的Bean对象。其中 excelReadHeadProperty.getHeadClazz()就是TestModel.class类型对象。map为读取到的 [属性名称 -> 单元格值]的key-value结构数据,当前为:"name" -> "张三"。而通过对BeanMap.create(resultModel).putAll(map)返回值调试得出 得到的testModel对象name属性为null。
这就是问题所在了,下面单独拿这段逻辑做单元测试。
CGLib的Map转Bean演示
BeanMap.create(resultModel).putAll(map)
@Test
public void testBeanMap() {
TestModel resultModel = new TestModel();
Map<String, Object> map = new HashMap<>();
map.put("name", "张三");
BeanMap.create(resultModel).putAll(map);
Assertions.assertThat(resultModel.getName()).isEqualTo("张三");
}
@Data
@Accessors(chain = true)
public static class TestModel{
@ExcelProperty("名字")
private String name;
}
输出结果如下:
Expecting:
<null>
to be equal to:
<"名字">
but was not.
Expected :名字
Actual :null
也就是name属性从map到bean的转换失败了。
CGLib是如何映射属性的
以下是BeanMap的部分源代码,包含了putAll方法的实现,从类信息可知BeanMap实现了Map接口并且是个抽象类。
abstract public class BeanMap implements Map
public void putAll(Map t) {
for (Iterator it = t.keySet().iterator(); it.hasNext();) {
Object key = it.next();
put(key, t.get(key));
}
}
public Object put(Object key, Object value) {
return put(bean, key, value);
}
abstract public Object put(Object bean, Object key, Object value);
}
继续调试发现,BeanMap根本没有继承类,也就是其子类是不存在。我们知道,一个类如果是抽象类就不能被实例化,也就是不能成为对象。而且当前BeanMap还有未实现的抽象方法。这就意味着BeanMap没有实例对象,且其非静态方法是无法被调用的。这就尴尬了,一个没有子类的抽象类是怎么被实例化的。这个问题就回到了CGLib身上了。
CGLib是什么
根据百科介绍:
CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。通常可以使用Java的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB是一个好的选择。
其原理如下:
动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。
实现方式:
使用字节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。
CGLib代理类查看
通过以上的说明,我们可以知道BeanMap的抽象类是通过CGLib生成了代理子类,并通过子类进行逻辑功能的执行,但是其子类是动态生成的,类信息是直接存储在内存中的,所以不能像我们调试源码一样调试CGlib代理对象。并且即使我们能从内存拿到CGLib生成的对象,其对象的类信息不一定存在SourceFile属性或者 LineNumberTable信息。
为了能看到CGLib生成的代理类源代码,在要生成代理类之前执行以下代码,设置cglib的代理类存储的文件路径:
// '/home/wanxp/Desktop/debug' 是生成的代理类存储的位置,改成你自己的文件地址即可
System.setProperty("cglib.debugLocation", "/home/wanxp/Desktop/debug");
执行完成代码之后,就能在指定的路径中看到
CGLib生成Bean代理类
package com.wanxp.test.Tests;
import com.wanxp.test.Tests.TestModel;
import java.util.Set;
import net.sf.cglib.beans.BeanMap;
import net.sf.cglib.beans.FixedKeySet;
public class Tests$TestModel$$BeanMapByCGLIB$$92a46992 extends BeanMap {
//其他逻辑...
public Object get(Object var1, Object var2) {
TestModel var10000 = (TestModel)var1;
String var10001 = (String)var2;
switch(((String)var2).hashCode()) {
case 3373707:
if (var10001.equals("name")) {
return var10000.getName();
}
}
return null;
}
public Object put(Object var1, Object var2, Object var3) {
TestModel var10000 = (TestModel)var1;
((String)var2).hashCode();
return null;
}
//其他逻辑...
}
Bean去除Accessors注解后CGLib生成代理类
package com.wanxp.test.Tests;
import com.wanxp.test.Tests.TestModel;
import java.util.Set;
import net.sf.cglib.beans.BeanMap;
import net.sf.cglib.beans.FixedKeySet;
public class Tests$TestModel$$BeanMapByCGLIB$$92a46992 extends BeanMap {
//其他逻辑...
public Object get(Object var1, Object var2) {
TestModel var10000 = (TestModel)var1;
String var10001 = (String)var2;
switch(((String)var2).hashCode()) {
case 3373707:
if (var10001.equals("name")) {
return var10000.getName();
}
}
return null;
}
public Object put(Object var1, Object var2, Object var3) {
TestModel var10000 = (TestModel)var1;
String var10001 = (String)var2;
switch(((String)var2).hashCode()) {
case 3373707:
if (var10001.equals("name")) {
String var10002 = var10000.getName(); //注意这里区别
var10000.setName((String)var3);
return var10002;
}
}
return null;
}
//其他逻辑...
}
Accessors注解的影响
从上面两段代码可以看出,其put方法不一致,有Accessor的注解则生成的代理类put方法中没有对TestModel属性name进行赋值,而无Accessor注解的则有对TestModel属性name进行赋值的逻辑。那为什么呢,我们知道Accessors(chain=true)意味着setter方法是有返回值。那进一步模拟无Accessors但setter有返回值cglib会如何处理。
CGLib的BeanMap如何处理模型有返回值的Setter
修改TestModel为以下
public static class TestModel{
private String name;
public String setName(String name) {
this.name = name;
return name;
}
}
生成的TestModel的代理类源码摘要如下:
public Object put(Object var1, Object var2, Object var3) {
TestModel2 var10000 = (TestModel2)var1;
((String)var2).hashCode();
return null;
}
public Object get(Object var1, Object var2) {
TestModel2 var10000 = (TestModel2)var1;
String var10001 = (String)var2;
switch(((String)var2).hashCode()) {
case 3373707:
if (var10001.equals("name")) {
return var10000.getName();
}
}
return null;
}
其put方法也没有对name 属性赋值,到这里就有点小明朗了。是CGLib对于setter方法有返回值的不认为是属性的setter。那是哪段逻辑生成这个代理类的,为什么它会认为带返回值的setter不是setter呢。那就进一步找到CGLib如何生成的逻辑。
BeanMap的TestModel代理类生成过程
BeanMap create(Object bean)传入TestModel对象,通过调用BeanMapEmitter的构造器初始化BeanMapEmitter对象,在BeanMapEmitter中使用java.beans.Introspector(这是jdk自带的类)获取TestModel.class的bean信息,最后调用Introspector.getTargetPropertyInfo()方法构建属性的getter/setter方法:
// Apply some reflection to the current class.
// First get an array of all the public methods at this level
Method methodList[] = getPublicDeclaredMethods(beanClass);
// Now analyze each method.
for (int i = 0; i < methodList.length; i++) {
Method method = methodList[i];
if (method == null) {
continue;
}
// skip static methods.
int mods = method.getModifiers();
if (Modifier.isStatic(mods)) {
continue;
}
String name = method.getName();
Class<?>[] argTypes = method.getParameterTypes();
Class<?> resultType = method.getReturnType();
int argCount = argTypes.length;
PropertyDescriptor pd = null;
if (name.length() <= 3 && !name.startsWith(IS_PREFIX)) {
// Optimization. Don't bother with invalid propertyNames.
continue;
}
try {
if (argCount == 0) {
if (name.startsWith(GET_PREFIX)) {
// Simple getter
pd = new PropertyDescriptor(this.beanClass, name.substring(3), method, null);
} else if (resultType == boolean.class && name.startsWith(IS_PREFIX)) {
// Boolean getter
pd = new PropertyDescriptor(this.beanClass, name.substring(2), method, null);
}
} else if (argCount == 1) {
if (int.class.equals(argTypes[0]) && name.startsWith(GET_PREFIX)) {
pd = new IndexedPropertyDescriptor(this.beanClass, name.substring(3), null, null, method, null);
} else if (void.class.equals(resultType) && name.startsWith(SET_PREFIX)) {
// Simple setter
pd = new PropertyDescriptor(this.beanClass, name.substring(3), null, method);
if (throwsException(method, PropertyVetoException.class)) {
pd.setConstrained(true);
}
}
} else if (argCount == 2) {
if (void.class.equals(resultType) && int.class.equals(argTypes[0]) && name.startsWith(SET_PREFIX)) {
pd = new IndexedPropertyDescriptor(this.beanClass, name.substring(3), null, null, null, method);
if (throwsException(method, PropertyVetoException.class)) {
pd.setConstrained(true);
}
}
}
} catch (IntrospectionException ex) {
// This happens if a PropertyDescriptor or IndexedPropertyDescriptor
// constructor fins that the method violates details of the deisgn
// pattern, e.g. by having an empty name, or a getter returning
// void , or whatever.
pd = null;
}
if (pd != null) {
// If this class or one of its base classes is a PropertyChange
// source, then we assume that any properties we discover are "bound".
if (propertyChangeSource) {
pd.setBound(true);
}
addPropertyDescriptor(pd);
}
}
上面方法是构建 Bean“属性描述”的逻辑,“属性描述”包含属性的setter与getter方法,获取setter与getter的逻辑是遍历Class下所有公开的方法,然后对方法名和方法入参与返回作比较,最后获取方法作为属性的setter与getter。
上述的setter/getter是使用jdk自带的反射方式获取的。对于jdk来说setter方法 来说需要符合以下这个条件才能作为setter方法:
void.class.equals(resultType) && name.startsWith("set")
即返回值为void并且以set开始的方法名才能成为setter。
综述
梳理以上的逻辑,jdk的获取setter的方式是返回值必须为void并且名字以set开始,所以导致CGLib获取TestModel的name属性setter时无法找到对应的方法,因为TestModel模型加了Accessor(chain = true)使setName方法返回的是当前模型对象TestModel。而EasyExcel是读取excel的数据并转换成Map,然后使用CGLib的BeanMap将Map转换成Bean对象的方式构建成Bean对象列表。逻辑过程如下:
EasyExcel 读取数据 ==> Map
CGLib 使用BeanMap将Map 转换==> Bean
BeanMap 调用JDK的拦截器获取setter方法
JDK 认为有返回值的set方法不是setter