记一次lombok链式调用引发EasyExcel兼容性的问题

发布于:2025-08-10 ⋅ 阅读:(14) ⋅ 点赞:(0)


前言

最近接手了一个项目,项目中需要用 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 {

    @Test
    public 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);
    }

    @Data
    public static class DemoVo {

        @ExcelProperty(value = "名称", index = 0)
        private String name;

        @ExcelProperty(value = "年龄", index = 1)
        private String age;

    }
}

这就是一个很简单的使用 easyExcel 读取 excel 表格数据的 Demo,大概逻辑就是拿到文件,然后使用 EasyExcelFactoryread 数据,再打印出来。

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 方法如下:

在这里插入图片描述

    @Test
    public 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#getWriteMethodjava.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 代理机制可能无法使其生效,因为这些操作绕过了代理类的拦截逻辑。


网站公告

今日签到

点亮在社区的每一天
去签到