一流的聊城做网站费用南宁公司注册
目录
- 前言
- 一、情景介绍
- 二、问题分析
- 三、解决问题
- 四、总结
前言
最近接手了一个项目,项目中需要用 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 代理机制可能无法使其生效,因为这些操作绕过了代理类的拦截逻辑。
