[异常]EasyExcel问题之Lombok的Accessors注解与CGLib BeanMap冲突

28

背景

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