在使用EasyExcel时发现在解析Excel文件时,发现单元格中的数据无法被注入到对象中,随后去EasyExcel项目的issues区发现这是个老问题了:https://github.com/alibaba/easyexcel/issues?q=Accessors

原因分析

在接收Excel数据的对象的构造方法上打断点,并查看方法的调用链可以看到这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private Object buildUserModel(Map<Integer, ReadCellData<?>> cellDataMap, ReadSheetHolder readSheetHolder,
AnalysisContext context) {
ExcelReadHeadProperty excelReadHeadProperty = readSheetHolder.excelReadHeadProperty();
Object resultModel;
try {
resultModel = excelReadHeadProperty.getHeadClazz().newInstance();
} catch (Exception e) {
throw new ExcelDataConvertException(context.readRowHolder().getRowIndex(), 0,
new ReadCellData<>(CellDataTypeEnum.EMPTY), null,
"Can not instance class: " + excelReadHeadProperty.getHeadClazz().getName(), e);
}
Map<Integer, Head> headMap = excelReadHeadProperty.getHeadMap();
BeanMap dataMap = BeanMapUtils.create(resultModel);
for (Map.Entry<Integer, Head> entry : headMap.entrySet()) {
Integer index = entry.getKey();
Head head = entry.getValue();
String fieldName = head.getFieldName();
if (!cellDataMap.containsKey(index)) {
continue;
}
ReadCellData<?> cellData = cellDataMap.get(index);
Object value = ConverterUtils.convertToJavaObject(cellData, head.getField(),
ClassUtils.declaredExcelContentProperty(dataMap, readSheetHolder.excelReadHeadProperty().getHeadClazz(),
fieldName), readSheetHolder.converterMap(), context, context.readRowHolder().getRowIndex(), index);
if (value != null) {
dataMap.put(fieldName, value);
}
}
return resultModel;
}

可以看到EasyExcel是使用cglib的BeanMap进行对象的属性进行赋值: dataMap.put(fieldName, value),问题就出现在生成的这个BeanMap对象上。

BeanMap为什么不能和@Accessors(chain = true)一起使用

BeanMap是一个抽象类,会由cglib生成具体的代理类。假设我们有这么一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@AllArgsConstructor
@NoArgsConstructor
public class Item {
@ExcelProperty(index = 0)
private String returnThis;
@ExcelProperty(index = 1)
private String returnVoid;

public Item setReturnThis(String returnThis) {
this.returnThis = returnThis;
return this;
}

public void setReturnVoid(String returnVoid) {
this.returnVoid = returnVoid;
}
}

@Accessors(chain = true)的作用就是对于lombok生成的setter方法,返回值不再是void而是对象自己即返回this

这样做的好处是使得setter方法支持链式调用,能够方便的在一行代码中同时对多个属性进行赋值,@Builder也能实现这个功能,但是会多生成一个内部静态类。

在类中的两个setter方法中打上断点,再次运行导入excel的方法,会发现返回void的set方法被调用了,而返回this的set方法没有被调用,猜想是cglib生成的BeanMap代理类有什么问题。

我们把cglib生成的类存到本地看看,设置cglib.debugLocation即可:

1
2
3
static{
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/tmp/cglib");
}

cglib类是在运行时生成的,所以我们使用static代码块对属性进行设置。

我们在com.alibaba.excel.read.listener.ModelBuildEventListener#buildUserModel这个方法上打上断点,方便我们定位生成的BeanMap类的类文件名:

可以很直观的看到是没有returnThis这个key的,并且我们知道了生成的cglib类文件保存为了Item$$BeanMapByEasyExcelCGLIB$$94beaa50.class,我们用Idea打开这个文件看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class Item$$BeanMapByEasyExcelCGLIB$$94beaa50 extends BeanMap {
private static FixedKeySet keys;
private static final Class CGLIB$load_class$java$2Elang$2EString;

public Item$$BeanMapByEasyExcelCGLIB$$94beaa50() {
}

public BeanMap newInstance(Object var1) {
return new Item$$BeanMapByEasyExcelCGLIB$$94beaa50(var1);
}

public Item$$BeanMapByEasyExcelCGLIB$$94beaa50(Object var1) {
super(var1);
}

public Object get(Object var1, Object var2) {
Item var10000 = (Item)var1;
((String)var2).hashCode();
return null;
}

public Object put(Object var1, Object var2, Object var3) {
Item var10000 = (Item)var1;
String var10001 = (String)var2;
switch (((String)var2).hashCode()) {
case 1337256676:
if (var10001.equals("returnVoid")) {
var10000.setReturnVoid((String)var3);
return null;
}
}

return null;
}

static {
CGLIB$STATICHOOK1();
keys = new FixedKeySet(new String[]{"returnVoid"});
}

static void CGLIB$STATICHOOK1() {
CGLIB$load_class$java$2Elang$2EString = Class.forName("java.lang.String");
}

public Set keySet() {
return keys;
}

public Class getPropertyType(String var1) {
switch (var1.hashCode()) {
case 1337256676:
if (var1.equals("returnVoid")) {
return CGLIB$load_class$java$2Elang$2EString;
}
}

return null;
}
}

我们可以看到以下两个特殊的地方:

  1. 对于keySet方法,是直接返回了一个static成员keys的值,keys在静态代码块中被初始化,并且只有returnVoid没有returnThis
  2. 对于put方法,switch代码块中只有returnVoid相关的代码

问题解决的办法

问题解决的办法很简单,只要不使用@Accessors(chain = true)即可,想要链式初始化对象的数据可以使用@Builder或者@SuperBuilder

使用@Builder有什么需要注意的地方么?

构造器(Builder)模式属于创建型设计模式之一,能够帮我们简化对于复杂对象的初始化,@Builder注解能够自动帮我们完成构造器模式的代码实现,看这段介绍一定会觉得lombok真的是太完美了,帮我们简化了非常多的代码,lombok确实很方便,但是有些地方我们需要注意一下。

依然是Item类,我们加上@Builder注解,并给每个属性一个初始的值:

1
2
3
4
5
6
7
8
9
10
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class Item {
@ExcelProperty(index = 0)
private String returnThis="this";
@ExcelProperty(index = 1)
private String returnVoid="void";
}

然后用Idea打开编译出来的.class文件,会发现lombok帮我们自动生成了一个Builder静态内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static class ItemBuilder {
private String returnThis;
private String returnVoid;

ItemBuilder() {
}

public ItemBuilder returnThis(final String returnThis) {
this.returnThis = returnThis;
return this;
}

public ItemBuilder returnVoid(final String returnVoid) {
this.returnVoid = returnVoid;
return this;
}

public Item build() {
return new Item(this.returnThis, this.returnVoid);
}

public String toString() {
return "Item.ItemBuilder(returnThis=" + this.returnThis + ", returnVoid=" + this.returnVoid + ")";
}
}

可以很直观的看出@Builder帮我们做了什么,在加上这个注解后,编译生成的.class文件中会加入一个ItemBuilder类,这个类的属性的Item属性一模一样,唯一的区别就是属性值没有默认值了,这就会导致我们build出来的对象的属性在没有指定值时都为null

能够解决这个问题么?可以

  1. 使用@Builder(toBuilder = true)即可,查看编译生成的.class文件时能够看到多了这样的代码:
1
2
3
public ItemBuilder toBuilder() {
return (new ItemBuilder()).returnThis(this.returnThis).returnVoid(this.returnVoid);
}

相应的我们的使用方法要做出改变,不能这么使用:

1
Item.builder().xxx.build()

而是应该这么使用:

1
new Item().toBuilder().xxx.build();
  1. 在有默认值的属性上加@Builder.Default注解

@Builder和@SuperBuilder有什么区别

通常我们会定义一个父类,将一些通用的属性抽出来,然后子类直接继承这个父类,就不需要重新定义重复的属性,但是对于@Builder注解我们会发现,子类的Builder类中是没有办法初始化到父类的属性的,想要实现Builder能够初始化父类的属性,可以使用@SuperBuilder

我们来看看@SuperBuilder做了什么,先来定义两个类Child和Father:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@AllArgsConstructor
@NoArgsConstructor
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
public class Child extends Father {
private String childField1;
private String childField2;
}

@AllArgsConstructor
@NoArgsConstructor
@Data
@SuperBuilder
abstract class Father {
private String fatherField1;
private String fatherField2;
}

查看编译生成的.class文件可以看到以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//Builder抽象静态内部抽象类
public abstract static class FatherBuilder<C extends Father, B extends FatherBuilder<C, B>> {
private String fatherField1;
private String fatherField2;

public FatherBuilder() {
}

protected abstract B self();

public abstract C build();

public B fatherField1(final String fatherField1) {
this.fatherField1 = fatherField1;
return this.self();
}

public B fatherField2(final String fatherField2) {
this.fatherField2 = fatherField2;
return this.self();
}

public String toString() {
return "Father.FatherBuilder(fatherField1=" + this.fatherField1 + ", fatherField2=" + this.fatherField2 + ")";
}
}
//新的构造器
protected Father(final FatherBuilder<?, ?> b) {
this.fatherField1 = b.fatherField1;
this.fatherField2 = b.fatherField2;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//Builde静态内部抽象类
public abstract static class ChildBuilder<C extends Child, B extends ChildBuilder<C, B>> extends Father.FatherBuilder<C, B> {
private String childField1;
private String childField2;

public ChildBuilder() {
}

protected abstract B self();

public abstract C build();

public B childField1(final String childField1) {
this.childField1 = childField1;
return this.self();
}

public B childField2(final String childField2) {
this.childField2 = childField2;
return this.self();
}

public String toString() {
String var10000 = super.toString();
return "Child.ChildBuilder(super=" + var10000 + ", childField1=" + this.childField1 + ", childField2=" + this.childField2 + ")";
}
}
//Builder静态内部实现类
private static final class ChildBuilderImpl extends ChildBuilder<Child, ChildBuilderImpl> {
private ChildBuilderImpl() {
}

protected ChildBuilderImpl self() {
return this;
}

public Child build() {
return new Child(this);
}
}
//新的构造器
protected Child(final ChildBuilder<?, ?> b) {
super(b);
this.childField1 = b.childField1;
this.childField2 = b.childField2;
}

public static ChildBuilder<?, ?> builder() {
return new ChildBuilderImpl();
}

@Builder不同,@SuperBuilder生成了一个抽象类和一个实现类,并且抽象类是泛型的,根据被注解的类的继承关系这个Builder抽象类会继承所有父类的Builder抽象类,最后实现类实现重现了继承关系的Builder抽象类。

此时这个实现类即使是初始化父类的属性,初始化方法返回的对象类型也是实现类的类型。

1
2
3
4
5
Child.builder()
.fatherField1("fatherField1")
.childField1("childField1")
.fatherField2("fatherField2")
.childField2("childField2");

@SuperBuilder同样支持toBuilder = true@Builder.Default

https://developer.aliyun.com/article/744593

https://github.com/spring-projects/spring-framework/issues/27802

https://github.com/spring-projects/spring-framework/issues/28110

https://blog.csdn.net/john1337/article/details/84653468