EasyExcel与@Accessors(chain = true)不兼容分析
EasyExcel
EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel. github地址: https://github.com/alibaba/easyexcel
Accessors
@Accessors 注解用来配置lombok如何产生和显示getters和setters的方法。
public @interface Accessors {
/**
* 如果为true,则访问器将以该字段命名,并且不包含 get 或 set *前缀,且省略了chain,则 chain 默认为true。
* *默认值:false
*/
boolean fluent() default false;
/**
* *如果为true,setter将返回this而不是void。
* *默认:false
*/
boolean chain() default false;
String[] prefix() default {};
}
测试案例
pom.xml
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.6</version>
</dependency>
Student.java
@Data
@Accessors(chain = true)
public class Student {
@ExcelProperty("编码")
private Long id;
@ExcelProperty("姓名")
private String name;
@ExcelProperty("性别")
private String sex;
@ExcelProperty("年龄")
private Integer age;
}
StudentControllerTest.java
public class StudentControllerTest {
/**
* 导出Excel上传模板
*/
@Test
public void importTemplate() {
String filePath = "D:/student.xlsx";
EasyExcel.write(filePath, Student.class).sheet("student").doWrite(null);
}
/**
* 导入Excel数据
*/
@Test
public void importExcel() {
String filePath = "D:/student.xlsx";
List<Object> list = EasyExcel.read(filePath, Student.class, null).sheet("student").doReadSync();
for (Object obj : list) {
System.out.println(obj.toString());
}
}
}
student.xlsx 数据
执行 importExcel 导入Excel 结果: 可以看到, 导入数据条数能解析, 但数据没有读取成功
原因详解
通过删减无关代码的方法, 发现当删减 @Accessors(chain = true)
或修改为 @Accessors(chain = false)
后, 能够正常读取到了, 定位到是该注解的影响, 为什么会影响呢? 带着这个疑问我们继续往下看
EasyExcel 解析Excel 过程
这里不就过多赘述了, 大家可以查看如下两个文章进行了解
EasyExcel对Excel文件的解析过程
easyexcel的源码简单分析
ModelBuildEventListener
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;
}
当Excel每行数据解析后, 会调用 buildStringList 将解析到的数据由Map转存到Bean中, 关键代码如下:
BeanMap.create(resultModel).putAll(map);
可见, EasyExcel是使用 net.sf.cglib.beans.BeanMap
工具类拷贝的, 这正是造成 使用@Accessors(chain = true)
后, EasyExcel解析不到数据的原因所在.
为什么使用了@Accessors(chain = true)
后, BeanMap会拷贝不成功呢?
@Accessors(chain = true) 的作用
使set方法返回的不是void, 而是当前对象, 例如:
// 不加 @Accessors(chain = true) 时
@Data
public class Student {
private String name;
}
// 相当于
public class Student {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
// 加了 @Accessors(chain = true) 时
@Data
@Accessors(chain = true)
public class Student {
private String name;
}
// 相当于, 添加的好处: 可以支持 new Student().setName("zhang").setXXX("xxx").setXXXX("XXX")这样的操作
public class Student {
private String name;
public String getName() {
return this.name;
}
public Student setName(String name) {
this.name = name;
return this;
}
}
BeanMap 从Map拷贝到Bean的测试
参考: 使用CGlib实现Bean拷贝(BeanCopier)
可见, 需要使用BeanMap从Map拷贝到Bean, 需要Map 的Key与Bean的变量名一致, 并有对应的 set方法, 且set方法为 void, 才能拷贝成功
已经找到了原因, 如何解决呢? 到GitHub里看了看, 作者表示现在还没法解决, 小编只能暂时不用 @Accessors(chain = true), 还是提醒广大网友, 在使用MybatisPlus生成代码时,一定要注意是否带有@Accessors(chain = true)注解,如果带有,会影响EasyExcel读取数据哦