前些天,有朋友在技术群里遇到了一个典型问题,场景是 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
解决方案
- 修改属性命名:遵循规范,确保属性的前两个字母都小写。例如,将
nMetaType 改为 nmetaType 或更符合习惯的 metaType。这是最根本的解决方案。
- 手动生成 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 在反射设置属性值时就会失败。
解决方案
- 移除
@Accessor(chain = true) 注解:如果不需要链式调用,直接移除该注解是最简单的办法。
- 等待框架适配:寄希望于
EasyExcel 或底层依赖库未来能支持识别非 void 返回类型的 setter 方法。
总结
Lombok 虽然极大提升了编码效率,但在与 Mybatis、EasyExcel 等强依赖 JavaBean 自省机制的框架集成时,可能会因为属性命名不规范或生成了非标准的 Getter/Setter 方法而引发隐蔽的 Bug。这要求开发者在享受便利的同时,也要对其行为有更深入的了解,特别是在涉及 数据库 交互和数据序列化/反序列化的场景下。无论是遵循严格的属性命名规范,还是在必要时手动编写部分代码,都是确保系统稳定性的有效手段。在 云栈社区 中,我们也经常讨论和分享这类框架集成时的“避坑”经验,更多实用的 技术文档 和解决方案,可以帮助开发者更顺利地完成项目。
