作为Lombok的信徒,我这次翻车了,啪啪打脸~
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
关注公众号后台回复pay或mall获取实战项目资料+视频
作者:liuxuzxx
来源:juejin.im/post/6881432532332576781
下面开始我们的正文。去年在某项目当中引入了 Lombok 插件,着实解放了双
手,代替了一些重复的简单工作(Getter,Setter,toString 等方法的编写)。
但是,在使用的过程当中,也发现了一些坑,开始的时候并没有察觉到是
Lombok 的问题,后来跟踪了对应的其他组件的源码,才发现是 Lombok 的问
问题发现
我们在项目当中主要使用 Lombok 的 Setter-Getter 方法的注解,也就是组合注
解 @Data,但是在一次使用 Mybatis 插入数据的过程当中,出现了一个问题。
我们有个实体类:
@Data
public class NMetaVerify{
private NMetaType nMetaType;
private Long id;
....其他属性
}
当我们使用 Mybatis 插入数据的时候,发现,其他属性都能正常的插入,但是
就是 nMetaType 属性在数据库一直是 null。
当我 Debug 项目代码到调用 Mybatis 的插入 SQL 对应的方法的时候,我
看到 NMetaVerify 对象的 nMetaType 属性还是有数据的,但是执行插入
之后,数据库的 nMetaType 字段就是一直是 null。
原先我以为是我的枚举类型写法不正确,看了下别的同样具有枚举类型的字段,
于是,我就跟踪 Mybatis 的源码,发现 Mybatis 在获取这个
nMetaType 属性的时候使用了反射,使用的是 getxxxx 方法来获取的。
但是我发现 nMetaType 的 get 方法好像有点和 Mybatis 需要的
原因
#Lombok生成的Get-Set方法
@Data
public class NMetaVerify {
private Long id;
private NMetaType nMetaType;
private Date createTime;
public void lombokFound(){
NMetaVerify nMetaVerify = new NMetaVerify();
nMetaVerify.setNMetaType(NMetaType.TWO); //注意:nMetaType的set方法为setNMetaType,第一个n字母大写了,
nMetaVerify.getNMetaType(); //getxxxx方法也是大写
}
}
#idea,Mybatis,Java官方默认的行为为:
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;
}
public NMetaType getnMetaType() {//注意:nMetaType属性的第一个字母小写
return nMetaType;
}
public void setnMetaType(NMetaType nMetaType) {//注意:nMetaType属性的第一个字母小写
this.nMetaType = nMetaType;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}
Mybatis(3.4.6 版本)解析 get-set 方法获取属性名字的源码:
package org.apache.ibatis.reflection.property;
import java.util.Locale;
import org.apache.ibatis.reflection.ReflectionException;
/**
* @author Clinton Begin
*/
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");
}
}
Mybatis 解析 get-set 方法为属性名字测试:
@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
解决方案
解决方案如下:
修改属性名字,让第二个字母小写,或者说是规定所有的属性的前两个字
母必须小写。
如果数据库已经设计好,并且前后端接口对接好了,不想修改,那就专门
为这种特殊的属性使用 idea 生成 get-set 方法。
@Accessor(chain = true)注解的问题
问题发现
在使用 easyexcel(github.com/alibaba/eas…)导出的时候,发现以前的实体类导
出都很正常,但是现在新加的实体类不正常了。
比对了发现,新加的实体类增加了 @Accessor(chain = true)注解,我们的目的主要是方便我们链式调用 set 方法:
new UserDto()
.setUserName("")
.setAge(10)
........
.setBirthday(new Date());
原因
easyexcel 底层使用的是 cglib 来做反射工具包的:
com.alibaba.excel.read.listener.ModelBuildEventListener 类的第130行
BeanMap.create(resultModel).putAll(map);
最底层的是cglib的BeanMap的这个方法调用
abstract public Object put(Object bean, Object key, Object value);
但是 cglib 使用的是 Java 的 rt.jar 里面的一个 Introspector 这个类的方法:
# Introspector.java 第520行
if (int.class.equals(argTypes[0]) && name.startsWith(GET_PREFIX)) {
pd = new IndexedPropertyDescriptor(this.beanClass, name.substring(3), null, null, method, null);
//下面这行判断,只获取返回值是void类型的setxxxx方法
} else 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);
}
}
解决方案
解决方案如下:
去掉 Accessor 注解。
要么就等待 easyexcel 的作者替换掉底层的 cglib 或者是其他,反正是支持
获取返回值不是 void 的 setxxx 方法就行。
有热门推荐👇
当MyBatis 3.5.X遇上JDK8竟然出现了性能问题,全项目组都得加班~
Spring Boot 项目打包 + Shell 脚本部署实践,太有用了!
8年开发,一直不知道 Java为什么要加 final 关键字!
点击阅读原文,前往学习SpringCloud实战项目