滥用Lombok的@EqualsAndHashCode导致线上事故复盘

滥用Lombok的@EqualsAndHashCode导致线上事故复盘

一、血的教训:那次生产事故,差点让我被祭天

去年双十一大促,电商团队线上系统频繁告警------订单去重逻辑失效,同一用户的库存被反复扣减,资金对账一塌糊涂。

连夜排查,根因竟是一行注解:@EqualsAndHashCode。

我们的OrderDTO用了@Data(包含@EqualsAndHashCode默认行为),里面有个updateTime字段。每次订单状态变更,updateTime都会更新。问题来了------

复制代码

java

复制代码
`@Data
public class OrderDTO {
    private Long orderId;
    private String userId;
    private LocalDateTime updateTime;  // ← 致命字段
}
`

同一个订单,更新前后updateTime不同 → hashCode()不同 → Redis缓存判定为不同key → 旧缓存清不掉,新缓存写不进 → 库存数据错乱。

更狠的还在后面。我们的User实体继承了BaseEntity:

复制代码

java

复制代码
`@Data
public class BaseEntity {
    protected String code;  // 业务编码
}

@Data
public class User extends BaseEntity {
    private String name;
}
`

两个User对象,code不同("U001" vs "U002"),但name相同(都叫"张三")。equals()竟然返回true!因为@Data生成的equals()默认只比较当前类字段,完全无视父类。

这不是bug,这是Lombok默认行为 ------callSuper = false。


二、事故全景:三个坑,三次翻车

坑1:缓存keyhash漂移,数据对不上

时间线 现象
10:00 订单创建,OrderDTO(id=1, time=10:00) 存入Redis
10:05 订单状态变更,updateTime刷新
10:05 查询时new了新对象OrderDTO(id=1, time=10:05)
10:05 hashCode()不同 → Redis判断为新key → 查不到旧缓存
10:05 重复扣库存,数据炸了

本质 :@EqualsAndHashCode默认把所有非静态字段纳入计算,updateTime这种易变字段就是定时炸弹。

坑2:继承体系equals语义崩坏

复制代码

java

复制代码
`Child c1 = new Child(); c1.setCode("A"); c1.setName("X");
Child c2 = new Child(); c2.setCode("B"); c2.setName("X");
System.out.println(c1.equals(c2));  // true!代码不同的两个人,竟然"相等"
`

业务语义上,code是用户唯一标识,name只是昵称。但Lombok默认生成的equals()只看name,导致两个完全不同的用户被判定为同一个人------权限越权、数据串户,全来了。

坑3:JPA双向关联导致StackOverflow

复制代码

java

复制代码
`@Entity
@EqualsAndHashCode  // 默认行为
public class User {
    @OneToMany(mappedBy = "user")
    private List<Order> orders;  // 双向关联
}

@Entity
public class Order {
    @ManyToOne
    private User user;
}
`

调用user.equals(anotherUser) → 比较orders → orders里每个Order比较user → 又比较orders......无限递归,栈溢出。

生产环境看到这个异常时,我整个人都不好了。


三、根因解剖:Lombok到底做了什么

@EqualsAndHashCode默认行为等价于:

复制代码

java

复制代码
`// 默认 callSuper = false, 所有非静态非transient字段参与
public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof User)) return false;
    User other = (User) o;
    // 只比较当前类声明的字段,父类字段?不存在的
    return Objects.equals(name, other.name) 
        && Objects.equals(age, other.age);
}
`

三个致命默认:

默认配置 后果
callSuper = false 继承体系中父类字段被无视,equals语义错误
所有非静态字段参与 易变字段(时间戳、临时状态)导致hash漂移
不排除关联字段 JPA双向关联导致无限递归/栈溢出

四、修复方案:四种姿势,对症下药

✅ 姿势1:只用主键比较(实体类首选)

复制代码

java

复制代码
`@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class User {
    @TableId
    @EqualsAndHashCode.Include
    private Long id;  // 只有id参与比较
    
    private String name;   // 不参与
    private String email;  // 不参与
    private LocalDateTime updateTime;  // 不参与
}
`

为什么这样做? 数据库记录的唯一标识是主键。即使其他字段全变了,id相同就是同一条记录。这符合实体类的语义,也彻底杜绝了hash漂移。

✅ 姿势2:排除捣乱字段

复制代码

java

复制代码
`@Data
@EqualsAndHashCode(exclude = {"updateTime", "createTime", "orders"})
public class Order {
    private Long id;
    private String status;
    private LocalDateTime updateTime;      // 排除
    @OneToMany(mappedBy = "order")
    private List<OrderItem> orders;        // 排除!避免栈溢出
}
`

✅ 姿势3:继承场景必须callSuper = true

复制代码

java

复制代码
`@Data
@EqualsAndHashCode
public class BaseEntity {
    protected String code;
}

@Data
@EqualsAndHashCode(callSuper = true)  // ← 关键!
public class User extends BaseEntity {
    private String name;
}
`

生成的equals逻辑:

复制代码

java

复制代码
`public boolean equals(Object o) {
    if (!super.equals(o)) return false;  // 先比较父类code
    User other = (User) o;
    return Objects.equals(name, other.name);  // 再比较子类name
}
`

注意 :父类也必须正确实现equals/hashCode,否则super.equals(o)退化成Object.equals()(比较引用),callSuper=true形同虚设。

✅ 姿势4:JPA实体用业务键,别用全字段

复制代码

java

复制代码
`@Entity
@Table(name = "t_user")
@Data
@EqualsAndHashCode(of = {"username"})  // 业务唯一键
public class User {
    @Id
    private Long id;
    private String username;  // 业务唯一标识
    private String password;
    private String nickname;
}
`

五、血泪总结:Lombok不是银弹,是裹着糖衣的手雷

场景 推荐配置 禁忌
数据库实体 onlyExplicitlyIncluded = true + @Include主键 ❌ 默认全字段比较
继承体系 callSuper = true(父子都加) ❌ 默认忽略父类
JPA实体 exclude关联字段 + of业务键 ❌ 包含@OneToMany/@ManyToOne
缓存key/Map键 只用不可变唯一标识 ❌ 包含时间戳等易变字段
DTO/VO 按需of或exclude ❌ 无脑@Data

最后一句话送给所有人:

Lombok生成的代码,你不看字节码就永远不知道它干了什么。线上每一次equals的"理所当然",都可能是下一次事故的导火索。用注解之前,先反编译看看它到底生成了什么。

这篇复盘,希望你永远用不上。但如果用上了------至少别在同一个坑里摔两次。

相关推荐
zhangzeyuaaa5 小时前
深入理解 Ruby 运算符:本质、分类、坑点与重载实战
开发语言·前端·ruby
一木 之林5 小时前
DeepSeek Agent 开发(一)
开发语言·前端·javascript
朔北之忘 Clancy5 小时前
青少年软编等考七级题解目录
开发语言·c++·青少年编程·gesp·csp·信奥赛·noi
ss2736 小时前
AI全栈实战 | 3.2-01 Python 基础:四大数据容器怎么选,推导式为什么是 Pythonic 的灵魂
开发语言·人工智能·python
Mr_Macallon6 小时前
C++回顾(基础)(01)
c语言·开发语言·c++·学习
未来之窗软件服务6 小时前
Go 利器 GoReSym:剥离符号恢复 Go 二进制元数据—东方仙盟
开发语言·golang·仙盟创梦ide·东方仙盟
朝朝辞暮i6 小时前
C++ 第 32 课:main()、rclcpp::init()、spin()、shutdown()
开发语言·c++·算法
旋生万物7 小时前
螺旋角分布的功率谱分析:多窗谱 + FDR 校正
开发语言·python
沫璃染墨7 小时前
《从零入门Linux系统篇(五十七):线程篇·十——生产者消费者模型进阶:从环形缓冲区到POSIX信号量》
linux·运维·服务器·开发语言·c++·系统架构·信号处理
时间的拾荒人7 小时前
Qt 窗口系统详解:从菜单栏到对话框的完整指南
开发语言·qt·面试