滥用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) → 比较ordersorders里每个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 按需ofexclude ❌ 无脑@Data

最后一句话送给所有人

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

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

相关推荐
yong99901 小时前
C# 实时查看硬件使用率(CPU 内存 硬盘 网络)
开发语言·网络·c#
不午休の野猫1 小时前
vs + qt环境编译.sln项目时报无法解析的外部符号metaObject && qt_metacast
开发语言·qt
吴声子夜歌2 小时前
Java——接口的细节
java·开发语言·算法
阿拉金alakin2 小时前
深入理解 Java 锁机制:CAS 原理、synchronized 优化与主流锁策略全总结
java·开发语言
myheartgo-on2 小时前
Java—方 法
java·开发语言·算法·青少年编程
雨落在了我的手上2 小时前
如何学习java?
java·开发语言·学习
神仙别闹3 小时前
基于 C# OpenPGP 的文件管理系统
开发语言·c#
番石榴AI3 小时前
纯 CPU 推理!0.1B 超轻量级端到端OCR模型,使用 Java 进行文档解析
java·开发语言·ocr
likerhood3 小时前
ConcurrentHashMap详细讲解(java)
java·开发语言·性能优化