lombok踩坑与思考
虽然接触到lombok已经有很长时间,但是大量使用lombok以减少代码编写还是在新团队编写新代码维护老代码中遇到的。
我个人并不主张使用lombok,其带来的代价足以抵消其便利,但是由于团队编码风格需要一致,用还是要继续使用下去。使用期间遇到了一些问题并进行了一番研究和思考,记录一下。
1. 一些杂七杂八的问题
这些是最初我不喜欢lombok的原因。
1.1 额外的环境配置
作为IDE插件+jar包,需要对IDE进行一系列的配置。目前在idea中配置还算简单,几年前在eclipse下也配置过,会复杂不少。
1.2 传染性
一般来说,对外打的jar包最好尽可能地减少三方包依赖,这样可以加快编译速度,也能减少版本冲突。一旦在resource包里用了lombok,别人想看源码也不得不装插件。
而这种不在对外jar包中使用lombok仅仅是约定俗成,当某一天lombok第一次被引入这个jar包时,新的感染者无法避免。
1.3 降低代码可读性
定位方法调用时,对于自动生成的代码,getter/setter还好说,找到成员变量后find usages,再根据上下文区分是哪种;equals()这种,想找就只能写段测试代码再去find usages了。
目前主流ide基本都支持自动生成getter/setter代码,和lombok注解相比不过一次键入还是一次快捷键的区别,实际减轻的工作量十分微小。
2. @EqualsAndHashCode和equals()
2.1 原理
当这个注解设置callSuper=true时,会调用父类的equlas()方法,对应编译后class文件代码片段如下:
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (!(o instanceof BaseVO)) {
return false;
} else {
BaseVO other = (BaseVO)o;
if (!other.canEqual(this)) {
return false;
} else if (!super.equals(o)) {
return false;
} else {
// 各项属性比较
}
}
}
如果一个类的父类是Object(java中默认没有继承关系的类父类都是Object),那么这里会调用Object的equals()方法,如下
public boolean equals(Object obj) {
return (this == obj);
}
2.2 问题
对于父类是Object且使用了@EqualsAndHashCode(callSuper = true) 注解的类,这个类由lombok生成的equals()方法只有在两个对象是同一个对象时,才会返回true,否则总为false,无论它们的属性是否相同。这个行为在大部分时间是不符合预期的,equals()失去了其意义。即使我们期望equals()是这样工作的,那么其余的属性比较代码便是累赘,会大幅度降低代码的分支覆盖率。以一个近6000行代码的业务系统举例,是否修复该问题并编写对应测试用例,可以使整体的jacoco分支覆盖率提高10%~15%。
相反地,由于这个注解在jacoco下只算一行代码,未覆盖行数倒不会太多。
2.3 解决
有几种解决方法可以参考:
- 不使用该注解。大部分pojo我们是不会调用equals进行比较的,实际用到时再重写即可。
- 去掉
callSuper = true。如果父类是Object,推荐使用。 - 重写父类的equals()方法,确保父类不会调用或使用类似实现的Ojbect的equals()。
2.4 其他
@data注解包含@EqualsAndHashCode注解,由于不调用父类equals(),避免了Object.equals()的坑,但可能带来另一个坑。详见@data章节。
3. @data
3.1 从一个坑出来掉到另一个大坑
上文提到@EqualsAndHashCode(callSuper = true) 注解的坑,那么 @data 是否可以避免呢?很不幸的是,这里也有个坑。
由于 @data 实际上就是用的 @EqualsAndHashCode,没有调用父类的equals(),当我们需要比较父类属性时,是无法比较的。示例如下:
@Data
public class ABO {
private int a;
}
@Data
public class BBO extends ABO {
private int b;
public static void main(String[] args) {
BBO bbo1 = new BBO();
BBO bbo2 = new BBO();
bbo1.setA(1);
bbo2.setA(2);
bbo1.setB(1);
bbo2.setB(1);
System.out.print(bbo1.equals(bbo2)); // true
}
}
很显然,两个子类忽略了父类属性比较。这并不是因为父类的属性对于子类是不可见——即使把父类private属性改成protected,结果也是一样——而是因为lombok自动生成的equals()只比较子类特有的属性。
3.2 解决方法
- 用了
@data就不要有继承关系,类似kotlin的做法,具体探讨见下一节 - 自己重写equals(),lombok不会对显式重写的方法进行生成
- 显式使用
@EqualsAndHashCode(callSuper = true)。lombok会以显式指定的为准。
3.3 关于@data和data
在了解了 @data 的行为后,会发现它和kotlin语言中的data修饰符有点像:都会自动生成一些方法,并且在继承上也有问题——前者一旦有继承关系就会踩坑,而后者修饰的类是final的,不允许继承。kotlin为什么要这样做,二者有没有什么联系呢?在一篇流传较广的文章(抛弃 Java 改用 Kotlin 的六个月后,我后悔了(译文))中,对于data修饰符,提到:
Kotlin 对 equals()、hashCode()、toString() 以及 copy() 有很好的实现。在实现简单的DTO 时它非常有用。但请记住,数据类带有严重的局限性。你无法扩展数据类或者将其抽象化,所以你可能不会在核心模型中使用它们。
这个限制不是 Kotlin 的错。在 equals() 没有违反 Liskov 原则的情况下,没有办法产生正确的基于值的数据。
对于Liskov(里氏替换)原则,可以简单概括为:
一个对象在其出现的任何地方,都可以用子类实例做替换,并且不会导致程序的错误。换句话说,当子类可以在任意地方替换基类且软件功能不受影响时,这种继承关系的建模才是合理的。
根据上一章的讨论,equals()的实现实际上是受业务场景影响的,无论是否使用父类的属性做比较都是有可能的。但是kotlin无法决定equals()默认的行为,不使用父类属性就会违反了这个原则,使用父类属性有可能落入调用Object.equals()的陷阱,进入了两难的境地。
kotlin的开发者回避了这个问题,不使用父类属性并且禁止继承即可。只是kotlin的使用者就会发现自己定义的data对象没法继承,不得不删掉这个关键字手写其对应的方法。
回过头来再看 @data ,它并没有避免这些坑,只是把更多的选择权交给开发者决定,是另一种做法。
4. 后记
其他lombok注解实际使用较少,整体阅读了 官方文档暂时没有发现其他问题,遇到以后继续更新。
实际上官方文档中也提到了equals()的坑。
lombok踩坑与思考的更多相关文章
- 记一次lombok踩坑记
引言 今天中午正在带着耳机遨游在代码的世界里,被运营在群里@了,气冲冲的反问我最近有删生产的用户数据的吗?我肯定客气的回答道没有呀?生产的数据我怎么能随随便便可以删除,这可是公司的红线,再说了我也没有 ...
- 一次flume exec source采集日志到kafka因为单条日志数据非常大同步失败的踩坑带来的思考
本次遇到的问题描述,日志采集同步时,当单条日志(日志文件中一行日志)超过2M大小,数据无法采集同步到kafka,分析后,共踩到如下几个坑.1.flume采集时,通过shell+EXEC(tail -F ...
- Lombok好用是好用,就是容易踩坑,这份避坑指南请查收
序言 各位好啊,我是会编程的蜗牛,作为java开发者,我们平常在开发过程中,总是希望能够尽量少敲代码.这一方面,当然是为了偷懒,另一方面,当然也是为了代码看起来更加简洁一点,不断往编程规范上靠.然后其 ...
- vue+ vue-router + webpack 踩坑之旅
说是踩坑之旅 其实是最近在思考一些问题 然后想实现方案的时候,就慢慢的查到这些方案 老司机可以忽略下面的内容了 1)起因 考虑到数据分离的问题 因为server是express搭的 自然少 ...
- JavaScript 踩坑心得— 为了高速(下)
一.前言 本文的上一篇 JavaScript 踩坑心得- 为了高速(上) 主要和大家分享的是 JavaScript 使用过程中的基本原则以及编写过程中的心得分享,本文主要和大家聊聊在各个使用场景下的 ...
- 后端路由项目由 gulp 改为 webpack 的踩坑实录
前言 公司有个后端路由的项目是用 gulp 作为前端自动化构建工具,最近学习了一下 webpack,深感其强大,一狠心将其改成了 webpack 构建,以下是踩坑实录. gulp 先来说说原来的架构. ...
- djangorestframework+vue-cli+axios,为axios添加token作为headers踩坑记
情况是这样的,项目用的restful规范,后端用的django+djangorestframework,前端用的vue-cli框架+webpack,前端与后端交互用的axios,然后再用户登录之后,a ...
- html2canvas截屏在H5微信移动端踩坑,ios和安卓均可显示
1.最近在做移动端开发,框架是vue,一产品需求是,后台返回数据,通过qrcode.js(代码比较简单,百度上已经很多了)生成二维码,然后通过html2canvas,将html元素转化为canvas, ...
- rsyslog磁盘辅助(Disk-Assisted)模式踩坑记
最近公司为方便tracing.排查, 搞全链路日志收集,而我手上的10亿+pv的动态前端服务必然在考虑之列. 之前呢. 都是运维定制的收集方式: 如上图,rsyslog push kafka, 优点嘛 ...
随机推荐
- Unity 3D中的阴影设置
在Unity 3D中,经常需要用到光照阴影,即Directional Light的Shadow,Shadow分为Hard Shadow和Soft Shadow.区别是Soft Shadow的阴影边缘比 ...
- 存储专栏:一句话说清RAID2.0
今天,西瓜哥来谈谈高端存储的一股势力,RAID 2.0,最近被华为HVS搞得风生水起,神奇的让人摸不着头脑.我还是从一个高端存储的江湖说起吧. 据说很久很久以前(别扔臭鸡蛋,讲故事都是这样的…),L ...
- python基础知识13---函数对象、函数嵌套、名称空间与作用域、装饰器
阅读目录 一 函数对象 二 函数嵌套 三 名称空间与作用域 四 闭包函数 五 装饰器 六 练习题 一 函数对象 1 函数是第一类对象,即函数可以当作数据传递 #1 可以被引用 #2 可以当作参数传递 ...
- Nginx的编译安装及选项
编译安装Nginx1.安装常见的工具和库(GCC.PCRE.zlib.OpenSSL) Nginx是一个由C语言编写的,所以需要一个编译工具如GNU的GCC[root@www ~]# yum inst ...
- 【rabbitmq】RabbitMQ 集群与网络分区
网络分区(network partitions) 官网-网络分区 网络设备故障导致的网络分裂.比如,存在A\B\C\D\E五个节点,A\B处于同一子网,B\C\D处于另外一子网,中间通过交换机相连.若 ...
- rabbitMQ 在 windows 64位环境下无法启动(提示乱码)的解决方法
执行start命令时,提示乱码 解决方法: Set the environment variable “RABBITMQ_BASE” to “c:\rabbitmq”, uninstall the s ...
- EMQ消息队列初体验
使用命令创建admin用户,密码123 emqx_ctl users add admin 配置规则/etc/emqx/acl.conf(除管理员,其他用户只能订阅限定的测试主题路径) %% 允许'ad ...
- note 11 字典
字典 Dictionary +什么是字典? +一系列的"键-值(key-value)"对 +通过"键"查找对应的"值" +类似纸质字典,通过 ...
- iptables的MAC地址过滤
这里(http://en.wikipedia.org/wiki/Mac_address)有关于MAC地址的一些信息. 查询现有设置 iptables -S [chain] 比如:针对1中所设 inp ...
- 深度学习(二)--深度信念网络(DBN)
深度学习(二)--深度信念网络(Deep Belief Network,DBN) 一.受限玻尔兹曼机(Restricted Boltzmann Machine,RBM) 在介绍深度信念网络之前需要先了 ...