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

2213

积分

0

好友

291

主题
发表于 2 小时前 | 查看: 4| 回复: 0

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

当初引入 Lombok 的初衷是为了简化代码,让那些重复的 Getter、Setter、toString 等方法自动生成,解放双手。但在实际使用过程中,尤其是在与其他框架(如 MybatisEasyExcel)集成时,一些隐蔽的问题开始浮现。起初,我们并未将这些异常与 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() 方法。当我查看 LomboknMetaType 生成的 get 方法时,发现它的命名和 Mybatis(或者说 JavaBean 规范)所期望的格式有所不同。

原因分析

Lombok 对于第一个字母小写、第二个字母大写的属性所生成的 Getter/Setter 方法,与 IDEAMybatis 所遵循的 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 作为反射工具包。问题出在 cglibBeanMap 最终依赖于 Java 标准库 rt.jar 中的 Introspector 类来分析和操作 Bean 的属性。

关键源码在 Introspector.java520 行附近:

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

表情图标




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

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

GMT+8, 2026-3-18 06:03 , Processed in 0.630442 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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