Lombok @Data注解趟坑计

1、背景描述

        开发过程中大家创建对象或者通过MyBatis插件自动反转生成实体类的时候,可能经常会使用Lombok的注解,Lombok是一个很优秀的Java库,简单的几个注解,可以干掉一大片的模版代码,比如@Data注解,可以减少很多get set方法,使代码看起来更加优雅,但是这个注解在有些情况下会有坑,我们在使用过程中还是要慎用,接下来,我会通过一个例子来复现这个坑

        前段时间在做一个需求,背景是一个包裹装了多个商品的情况,测试反馈在出库的时候报错了,第一反应就是去看出库相关的代码,发现都是一年前的代码,然后就去查数据库,发现包裹关联的数据没有对应的出库单明细,想来肯定是生成包裹的时候,更新的数据不对,然后去看拣货下架的代码,发现也都是一年前的代码,感觉十分诡异,然后在测试环境加了很多日志,抓取日志报文后,一通debug,最后发现是Lombok @Data注解的坑

 2、这个坑是什么呢?

        子类对象如果只添加了@Data注解的时候,会自动重写hashCode() equals() toString()方法方法,且这些方法不关注父对象属性,多个子类对象,如果只有父类属性有值,计算出来的hash值会相同,如果有使用hashCode() equals()的地方,可能会有问题。

 3、问题复现

        创建父类Person

@Data
public class Person {

    private Integer id;

    private String name;

    private String sex;
}

        创建子类Student,手写get set方法

public class Student extends Person {

    private String school;

    private Integer grade;

    public String getSchool() {
        return school;
    }

    public void setSchool(String school) {
        this.school = school;
    }

    public Integer getGrade() {
        return grade;
    }

    public void setGrade(Integer grade) {
        this.grade = grade;
    }
}

        测试Demo  BeanDataDemo

@Slf4j
public class BeanDataDemo {

    public static void main(String[] args) {
        List<Student> studentList = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Student student = new Student();
            student.setId(i);
            student.setName("小明" + i);
            student.setSex(i % 2 == 0 ? "男" : "女");
            studentList.add(student);
        }
        log.info("studentList {}", JSONUtil.toJsonStr(studentList));
        List<Student> distinctStudentList = studentList.stream().distinct().collect(Collectors.toList());
        log.info("distinctStudentList {}", JSONUtil.toJsonStr(distinctStudentList));
    }

}

        运行结果

studentList [{"sex":"男","name":"小明0","id":0},{"sex":"女","name":"小明1","id":1},{"sex":"男","name":"小明2","id":2},{"sex":"女","name":"小明3","id":3},{"sex":"男","name":"小明4","id":4}]
distinctStudentList [{"sex":"男","name":"小明0","id":0},{"sex":"女","name":"小明1","id":1},{"sex":"男","name":"小明2","id":2},{"sex":"女","name":"小明3","id":3},{"sex":"男","name":"小明4","id":4}]

        我们把Student的get set方法去掉,改用@Data注解

@Data
public class Student extends Person {

    private String school;

    private Integer grade;
}

        再次运行 测试Demo  BeanDataDemo 得到运行结果

studentList [{"sex":"男","name":"小明0","id":0},{"sex":"女","name":"小明1","id":1},{"sex":"男","name":"小明2","id":2},{"sex":"女","name":"小明3","id":3},{"sex":"男","name":"小明4","id":4}]
distinctStudentList [{"sex":"男","name":"小明0","id":0}]

        通过日志对比,我们发现,手写get set方法的情况下,studentList和distinctStudentList经过stream().distinct()去重前后都是一致的,但加了@Data注解之后,studentList经过stream().distinct()去重之后,distinctStudentList只剩一个对象了,但在stream().distinct()之前,studentList对象的属性是不同的呀,这到底是为什么呢?

        我们知道,stream().distinct()去重原理是靠hashcode()方法定位槽,equals()方法判断是否是同一个对象,如果是则排重被去掉,不是的话保留,通过debug,我们发现,虽然student的父类属性不同,但多个对象的hashCode确是相同的,所以distinct之后就只剩一个对象了

         那么问题来了,为什么属性不同的对象,hashCode确是相同呢?手写get set方法的情况下为什么没问题呢?于是我们看了编译后的.class文件

有@Data注解的 Student.class

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.xm.study.demo.utils;

public class Student extends Person {
    private String school;
    private Integer grade;

    public Student() {
    }

    public String getSchool() {
        return this.school;
    }

    public Integer getGrade() {
        return this.grade;
    }

    public void setSchool(final String school) {
        this.school = school;
    }

    public void setGrade(final Integer grade) {
        this.grade = grade;
    }

    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof Student)) {
            return false;
        } else {
            Student other = (Student)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                Object this$grade = this.getGrade();
                Object other$grade = other.getGrade();
                if (this$grade == null) {
                    if (other$grade != null) {
                        return false;
                    }
                } else if (!this$grade.equals(other$grade)) {
                    return false;
                }

                Object this$school = this.getSchool();
                Object other$school = other.getSchool();
                if (this$school == null) {
                    if (other$school != null) {
                        return false;
                    }
                } else if (!this$school.equals(other$school)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(final Object other) {
        return other instanceof Student;
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $grade = this.getGrade();
        int result = result * 59 + ($grade == null ? 43 : $grade.hashCode());
        Object $school = this.getSchool();
        result = result * 59 + ($school == null ? 43 : $school.hashCode());
        return result;
    }

    public String toString() {
        return "Student(school=" + this.getSchool() + ", grade=" + this.getGrade() + ")";
    }
}

   手写get set方法的 Student.class

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.xm.study.demo.utils;

public class Student extends Person {
    private String school;
    private Integer grade;

    public Student() {
    }

    public String getSchool() {
        return this.school;
    }

    public void setSchool(String school) {
        this.school = school;
    }

    public Integer getGrade() {
        return this.grade;
    }

    public void setGrade(Integer grade) {
        this.grade = grade;
    }
}

        通过对比class文件,我们发现,添加@Data注解之后,默认会重写子类的 hashCode() equals() toString()方法,而且这些方法中只关注了子类的属性,没有父类的属性,我们在测试Demo中只给Student的父类属性赋值了,子类属性均为默认值,这样就解释了,为什么我们在studentList中不管获取哪个对象,他的hashCode都是相同的,随后我们去查了Lombok的官方文档,发现官方文档有写,可以通过注解@EqualsAndHashCode(callSuper = false/true) 中的callSuper来指定是否关注父类属性

        随后,我们对加了@Data注解的Student 类稍作改造

@Data
@EqualsAndHashCode(callSuper = true)
public class Student extends Person {

    private String school;

    private Integer grade;
}

        然后,再次debug运行测试Demo BeanDataDemo ,并获取到运行结果

studentList [{"sex":"男","name":"小明0","id":0},{"sex":"女","name":"小明1","id":1},{"sex":"男","name":"小明2","id":2},{"sex":"女","name":"小明3","id":3},{"sex":"男","name":"小明4","id":4}]
distinctStudentList [{"sex":"男","name":"小明0","id":0},{"sex":"女","name":"小明1","id":1},{"sex":"男","name":"小明2","id":2},{"sex":"女","name":"小明3","id":3},{"sex":"男","name":"小明4","id":4}]

        这时候我们在去看加了@Data和@EqualsAndHashCode(callSuper = true)注解的Student.class,会发现equals()方法中多了一行关于父类的equals()判断 !super.equals(o) ,hashCode()方法中多了一行关于父类的hashCode() super.hashCode()

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.xm.study.demo.utils;

public class Student extends Person {
    private String school;
    private Integer grade;

    public Student() {
    }

    public String getSchool() {
        return this.school;
    }

    public Integer getGrade() {
        return this.grade;
    }

    public void setSchool(final String school) {
        this.school = school;
    }

    public void setGrade(final Integer grade) {
        this.grade = grade;
    }

    public String toString() {
        return "Student(school=" + this.getSchool() + ", grade=" + this.getGrade() + ")";
    }

    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof Student)) {
            return false;
        } else {
            Student other = (Student)o;
            if (!other.canEqual(this)) {
                return false;
            } else if (!super.equals(o)) {
                return false;
            } else {
                Object this$grade = this.getGrade();
                Object other$grade = other.getGrade();
                if (this$grade == null) {
                    if (other$grade != null) {
                        return false;
                    }
                } else if (!this$grade.equals(other$grade)) {
                    return false;
                }

                Object this$school = this.getSchool();
                Object other$school = other.getSchool();
                if (this$school == null) {
                    if (other$school != null) {
                        return false;
                    }
                } else if (!this$school.equals(other$school)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(final Object other) {
        return other instanceof Student;
    }

    public int hashCode() {
        int PRIME = true;
        int result = super.hashCode();
        Object $grade = this.getGrade();
        result = result * 59 + ($grade == null ? 43 : $grade.hashCode());
        Object $school = this.getSchool();
        result = result * 59 + ($school == null ? 43 : $school.hashCode());
        return result;
    }
}

        至此,问题分析完毕并解决,针对这种父子关系的实体类,我们的解决方式有两种

        ①、不使用@Data注解,手写get set方法,根据需要看是否需要重写hashCode和equals方法

        ②、同时使用@Data注解和@EqualsAndHashCode(callSuper = true)