找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4764

积分

0

好友

604

主题
发表于 2026-3-18 03:27:38 | 查看: 400| 回复: 0

前些天,有朋友在技术群里遇到了一个典型问题,场景是 Spring 接收字段失败,原因在于字段名的第一个字母小写,第二个字母大写。这让我想起了之前项目里也踩过类似的“坑”,而根源竟出在大家常用的 Lombok 插件上。

当初引入 Lombok 的初衷是为了简化代码,让那些重复的 Getter、Setter、toString 等方法自动生成,解放双手。但在实际使用过程中,尤其是在与其他框架(如 Mybatis、EasyExcel)集成时,一些隐蔽的问题开始浮现。起初,我们并未将这些异常与 Lombok 联系起来,直到一步步追踪相关组件的源码,才发现问题所在。

技术讨论聊天截图

Setter/Getter 方法的坑

问题发现

我们在项目中主要使用 Lombok 的组合注解 @Data 来生成 Setter/Getter。在一次使用 Mybatis 插入数据时,遇到了一个奇怪的现象:有一个实体类,其他属性都能正常入库,唯独一个名为 nMetaType 的属性,数据库里对应的字段值始终是 null。

实体类如下:

@Data
public class NMetaVerify {
    private NMetaType nMetaType;
    private Long id;
    // ....其他属性
}

解决过程

通过 debug 跟踪到 Mybatis 执行插入 SQL 的地方,确认 NMetaVerify 对象的 nMetaType 属性在传入时是有值的。但执行完成后,数据库里就是 null。起初怀疑是枚举类型处理有误,但对比其他同样使用了枚举的字段,却都能正常插入。

于是,开始追踪 Mybatis 的源码。发现 Mybatis 在通过反射获取对象属性时,使用的是标准的 getXxx() 方法。当我查看 Lombok 为 nMetaType 生成的 get 方法时,发现它的命名和 Mybatis(或者说 JavaBean 规范)所期望的格式有所不同。

原因分析

Lombok 对于第一个字母小写、第二个字母大写的属性所生成的 Getter/Setter 方法,与 IDEA、Mybatis 所遵循的 Java 官方默认行为存在差异。

Lombok 生成的 Get-Set 方法:

@Data
public class NMetaVerify {
    private Long id;
    private NMetaType nMetaType;
    private Date createTime;

    public void lombokFound() {
        NMetaVerify nMetaVerify = new NMetaVerify();
        // 注意:nMetaType的set方法为setNMetaType,第一个n字母大写了,
        nMetaVerify.setNMetaType(NMetaType.TWO);
        // getxxxx方法也是大写
        nMetaVerify.getNMetaType();
    }
}

IDEA、Mybatis 及 Java 官方默认行为生成的 Get-Set 方法:

public class NMetaVerify {
    private Long id;
    private NMetaType nMetaType;
    private Date createTime;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    // 注意:nMetaType属性的getter方法,第一个字母n是小写的
    public NMetaType getnMetaType() {
        return nMetaType;
    }

    // 注意:nMetaType属性的setter方法,第一个字母n也是小写的
    public void setnMetaType(NMetaType nMetaType) {
        this.nMetaType = nMetaType;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}

核心源码解读

问题的关键在于 Mybatis(以 3.4.6 版本为例)如何从方法名解析出属性名。相关逻辑位于 org.apache.ibatis.reflection.property.PropertyNamer 类的 methodToProperty 方法中:

package org.apache.ibatis.reflection.property;

import java.util.Locale;
import org.apache.ibatis.reflection.ReflectionException;

public final class PropertyNamer {

    private PropertyNamer() {
        // Prevent Instantiation of Static Class
       }

    public static String methodToProperty(String name) {
        if (name.startsWith("is")) { // is开头的一般是bool类型,直接从第二个(索引)开始截取(简单粗暴)
          name = name.substring(2);
      } else if (name.startsWith("get") || name.startsWith("set")) { // set-get的就从第三个(索引)开始截取
          name = name.substring(3);
      } else {
        throw new ReflectionException("Error parsing property name '" + name + "'.  Didn't start with 'is', 'get' or 'set'.");
      }
        // 下面这个判断很重要
        // 第一句话:name.length()==1
        //       对于属性只有一个字母的,例如private int x;
        //           对应的get-set方法是getX();setX(int x);
        // 第二句话:name.length() > 1 && !Character.isUpperCase(name.charAt(1)))
        //      属性名字长度大于1,并且第二个(代码中的charAt(1),这个1是数组下标)字母是小写的
        //      如果第二个char是大写的,那就直接返回name
        if (name.length() == 1 || (name.length() > 1 && !Character.isUpperCase(name.charAt(1)))) {
            // 让属性名第一个字母小写,然后加上后面的内容
            name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
        }

        return name;
    }

    public static boolean isProperty(String name) {
        return name.startsWith("get") || name.startsWith("set") || name.startsWith("is");
    }

    public static boolean isGetter(String name) {
        return name.startsWith("get") || name.startsWith("is");
    }

    public static boolean isSetter(String name) {
        return name.startsWith("set");
    }

}

关键逻辑解读:当方法名去掉“get”或“set”前缀后,如果剩余部分(即属性名)第二个字母是大写的,Mybatis 会认为这是一个“特殊”的命名(比如 NMetaType),它会直接返回这个“原样”的名字(NMetaType),而不会再将其首字母转为小写。然而,Java Introspector 等标准机制期望的 getter 方法名是 getnMetaType(首字母小写),属性名对应为 nMetaType。这就导致了 Mybatis 拿着 getNMetaType 找不到对应的属性 nMetaType,自然也就无法获取和设置其值。

我们可以写个简单的测试来验证:

@Test
public void foundPropertyNamer(){
    String isName = "isName";
    String getName = "getName";
    String getnMetaType = "getnMetaType";
    String getNMetaType = "getNMetaType";

    Stream.of(isName,getName,getnMetaType,getNMetaType)
            .forEach(methodName->System.out.println("方法名字是:"+methodName+" 属性名字:"+ PropertyNamer.methodToProperty(methodName)));
}

输出结果如下:

    方法名字是:isName 属性名字:name
    方法名字是:getName 属性名字:name
    方法名字是:getnMetaType 属性名字:nMetaType //这个以及下面的属性第二个字母都是大写,所以直接返回name
    方法名字是:getNMetaType 属性名字:NMetaType

解决方案

  1. 修改属性命名:遵循规范,确保属性的前两个字母都小写。例如,将 nMetaType 改为 nmetaType 或更符合习惯的 metaType。这是最根本的解决方案。
  2. 手动生成 Getter/Setter:如果因数据库设计或前后端接口已定而无法修改属性名,可以为这类特殊属性使用 IDEA 手动生成 Getter/Setter 方法,覆盖 Lombok 的自动生成。

@Accessor(chain = true) 注解的问题

问题发现

在使用 EasyExcel 进行数据导出时,我们发现新添加的实体类导出异常,而旧的实体类正常。经过对比,发现新实体类上多了一个 @Accessor(chain = true) 注解。添加这个注解的本意是为了实现链式调用,让代码更简洁:

new UserDto()
.setUserName("")
.setAge(10)
........
.setBirthday(new Date());

原因分析

EasyExcel 底层使用 cglib 作为反射工具包。问题出在 cglib 的 BeanMap 最终依赖于 Java 标准库 rt.jar 中的 Introspector 类来分析和操作 Bean 的属性。

关键源码在 Introspector.java 第 520 行附近:

if (void.class.equals(resultType) && name.startsWith(SET_PREFIX)) {
    // Simple setter
    pd = new PropertyDescriptor(this.beanClass, name.substring(3), null, method);
    if (throwsException(method, PropertyVetoException.class)) {
       pd.setConstrained(true);
    }
}

请注意这个判断条件:void.class.equals(resultType)。Introspector 在识别 setter 方法时,只认返回值是 void 类型的方法。而 Lombok 的 @Accessor(chain = true) 生成的 setter 方法,返回值是对象本身(即 return this;),以便支持链式调用。这导致 Introspector 不认为这些方法是有效的 setter,进而 EasyExcel 在反射设置属性值时就会失败。

解决方案

  1. 移除 @Accessor(chain = true) 注解:如果不需要链式调用,直接移除该注解是最简单的办法。
  2. 等待框架适配:寄希望于 EasyExcel 或底层依赖库未来能支持识别非 void 返回类型的 setter 方法。

总结

Lombok 虽然极大提升了编码效率,但在与 Mybatis、EasyExcel 等强依赖 JavaBean 自省机制的框架集成时,可能会因为属性命名不规范或生成了非标准的 Getter/Setter 方法而引发隐蔽的 Bug。这要求开发者在享受便利的同时,也要对其行为有更深入的了解,特别是在涉及 数据库 交互和数据序列化/反序列化的场景下。无论是遵循严格的属性命名规范,还是在必要时手动编写部分代码,都是确保系统稳定性的有效手段。在 云栈社区 中,我们也经常讨论和分享这类框架集成时的“避坑”经验,更多实用的 技术文档 和解决方案,可以帮助开发者更顺利地完成项目。

表情图标




上一篇:从3万月税看英伟达薪资:股票激励如何绑定顶尖人才
下一篇:基于OpenClaw与全开源EDA工具,我成功设计了一颗RISC-V SoC芯片
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-30 17:00 , Processed in 0.459344 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表