记一次lombok链式调用引发EasyExcel兼容性的问题
目录
- 前言
- 一、情景介绍
- 二、问题分析
- 三、解决问题
- 四、总结
前言
最近接手了一个项目,项目中需要用 easyExcel
来读取导入的文件,本来是一个很简单的问题,但是发现怎么都读取不到文件中的内容,最后发现是因为 lombok
链式调用引起 setter
方法失效,导致数据没有正确 读
出来。
于是乎记录下排查的过程,避免下次踩坑 ~~
一、情景介绍
项目源码不能展示,我写了一个 Demo
也能达到一样的效果,代码如下:
import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import org.junit.jupiter.api.Test;import java.io.File;
import java.util.List;public class DemoTest {@Testpublic void test_1() {File file = new File("D:\\test\\demo.xlsx");List<DemoVo> voList = EasyExcelFactory.read(file, DemoVo.class, null).sheet().doReadSync();System.out.println("voList = " + voList);}@Datapublic static class DemoVo {@ExcelProperty(value = "名称", index = 0)private String name;@ExcelProperty(value = "年龄", index = 1)private String age;}
}
这就是一个很简单的使用 easyExcel
读取 excel
表格数据的 Demo
,大概逻辑就是拿到文件,然后使用 EasyExcelFactory
去 read
数据,再打印出来。
D:\\test\\demo.xlsx
表格文件内容如下:
就一个表头加上一行数据,运行之后结果如下:
什么!居然没有读到表格中的数据!从代码上看也没有看出什么问题,那是为什么呢?
二、问题分析
为什么表格中的数据没有读到?
为了确定表格中的数据是否读到,我在 DemoVo 的类上手写了一个 setName 方法,并且打上了断点,因为 easyExcel 在解析到数据之后会通过 setter 方法将值写到指定对象的属性当中,也就是说肯定会走属性的 setter 方法,只要走进来了,我就能通过参数是否存在数据判断是否正确读到了表格中的数据
Debug
运行
运行之后发现入参是有值的,也就是说明实际上是读到表格中的数据了,恢复程序继续运行
运行结束之后,惊奇的事情来了,我手动写了 setName
方法 voList
集合中的 name
属性的值就正确打印出来了,而 age
属性的值依旧为 null
@Data 注解不是会自动生成 getter 和 setter 方法吗,那为什么 setAge 方法没有生效?
于是乎我又写了一个test_2
方法去验证 @Data
生成的 setter
方法是否生效
运行 test_2
方法
从运行结果上来看 @Data
生成的 setter
方式是能够生效的
于是乎,问题就变成了:使用 @Data 生成的 setter 方法为什么会在 easyExcel 读取文件并写入数据的时候会失效?
继续断点运行 test_1
通过当前帧的堆栈追踪,可以看到 setter
方法是在 com.alibaba.excel.read.listener.ModelBuildEventListener#buildUserModel
方法中的 dataMap.put(fieldName, value);
这行代码设置的,在该处打上断点继续运行
可以看到表格中年龄对应的数据 18
也是正确读出来的,那为什么 dataMap.put(“age”, “18”); 就没有生效呢?
翻阅 com.alibaba.excel.read.listener.ModelBuildEventListener#buildUserModel
方法发现 dataMap
是通过 BeanMap dataMap = BeanMapUtils.create(resultModel);
创建的,那我们可以再写一个 test_3
去验证: BeanMap 是不是引起的 setAge 方法失效的罪魁祸首?
test_3 方法如下:
@Testpublic void test_3() throws InstantiationException, IllegalAccessException {DemoVo demoVo = DemoVo.class.newInstance();BeanMap dataMap = BeanMapUtils.create(demoVo);dataMap.put("name", "张三");dataMap.put("age", "18");System.out.println(dataMap);}
运行 test_3
方法
发现对于 @Data 注解生成的 setAge 方法在 org.springframework.cglib.beans.BeanMap#put(java.lang.Object, java.lang.Object)
方法中调用就是没有生效的。
为什么自己写的 setName 方法就能生效呢?
于是我又 Debug
运行 test_3
通过当前帧的堆栈追踪,发现 setter
方法是通过 DemoVo
的代理对象进行调用的
那就通过 System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, filePath);
将该代理类的字节码打印出来,代码如下:
// 生成的代理类打印到 D:\static\class 文件夹下
String filePath = "D:\\static\\class";
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, filePath);
运行 test_3
在 D:\static\class
目录下找到其代理对象
查看所生成的代理对象,发现其 put(Object var1, Object var2, Object var3)
方法中只有给 name
属性设置值,没有给 age
属性设置值的代码,所有就造成了 dataMap.put("age", "18");
这行代码并没有生效。
那问题又来了,为什么手写的 setName 方法就能让 BeanMap 生成的代理对象有对应的 set 逻辑,但是 @Data 生成的 setAge 方法却没有正常生成对应的 set 逻辑呢?
通过代理对象的创建过程可知:
我们要看 BeanMap 是如何生成代理对象的字节码对象的
代理对象生成字节码是在 org.springframework.cglib.core.ClassGenerator#generateClass
方法中实现的
那就在 org.springframework.cglib.beans.BeanMap
类中找下 generateClass
方法
org.springframework.cglib.beans.BeanMap.Generator#generateClass
,源码如下:
那就去找 setter
方法生成的逻辑
org.springframework.cglib.beans.BeanMapEmitter#BeanMapEmitter
,源码如下:
org.springframework.cglib.core.ReflectUtils#getBeanSetters
,源码如下:
org.springframework.cglib.core.ReflectUtils#getPropertiesHelper
,源码如下:
java.beans.PropertyDescriptor#getWriteMethod
、java.beans.PropertyDescriptor#setWriteMethod
,源码如下:
也就是说 BeanMap
是否生成对象的 set
逻辑和其本身的 setter
方法是否存在返回值有关系
那就看下对象 DemoVo
的字节码,看看其 setAga
方法是否存在返回值
从 DemoVo
的字节码对象可见,原来 @Data
生成的 setter
方法是存在返回值的,返回值为 this
那为什么 @Data 生成的 setter 方法会带返回值呢?
其实并不是 @Data
方法生成的 setter
方法会带返回,而是因为 lombok
的另外一个注解 @Accessors(chain = true)
,该注解的作用是用于修改生成的 setter
方法的行为。能够使 setter
方法返回当前对象(this
)而不是 void
,从而支持链式调用。
@Accessors(chain = true)
是可以全局配置的,可以通过在项目的根目录(通常是和src
目录同级)创建一个 lombok.config
文件来实现
在 lombok.config
文件中,我们可以设置全局的访问器属性。例如,要全局启用链式 setter 方法,可以添加如下配置:
config.stopBubbling = true # 停止 Lombok 向父目录搜索配置文件
lombok.accessors.chain = true
这样,整个项目中的所有类的所有字段在生成 setter
方法时都会采用链式风格(即返回 this
)。
于是在项目中确实找到了 lombok.config
文件,并且该文件中存在着这行配置
真相终于大白了,就是这个问题导致的
三、解决问题
竟然是因为 lombok.accessors.chain = true
配置导致类中的 setter 方法的返回值变成 this,那就直接在 DemoVo
的类上添加 @Accessors(chain = false)
就应该能够解决这个问题。
运行结果如下:
就能看到 excel
的数据被正常 读
出来了。
以上就是该问题的分析过程与处理方式。
四、总结
@Accessors(chain = true)
并没有使 setter
"失效"
,而是创建了一种不符合传统 Java Bean
规范的 setter
变体。当与依赖标准 Bean
规范的框架(如 EasyExcel
)一起使用时,会导致兼容性问题。
如果存在链式调用的 set
方法,那么通过 BeanMap
或者其他方式直接操作代理类的属性时,cglib
代理机制可能无法使其生效,因为这些操作绕过了代理类的拦截逻辑。