EasyExcel与@Accessors,@Builder与@SuperBuilder
发表于|更新于
|阅读量:
在使用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; } }
|
我们可以看到以下两个特殊的地方:
- 对于keySet方法,是直接返回了一个static成员
keys
的值,keys
在静态代码块中被初始化,并且只有returnVoid
没有returnThis
。
- 对于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。
能够解决这个问题么?可以
- 使用
@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();
|
- 在有默认值的属性上加
@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
| 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
| 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 + ")"; } }
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