使用Hibernate、JPA、Lombok遇到的有趣问题

6,904 阅读9分钟

前言

先用我不是药神电影海报镇楼,这个电影真心不错,推荐大家。

image.png

准备

讲解Hibernate之前,首先创建两个实体类,一个是Student类,一个School类。School和Student的关系是一对多的关系

@Entity
@Table(name = "tbl_school")
@Data
public class School {

    @Id
    @GenericGenerator(name = "idGenerator", strategy = "uuid")
    @GeneratedValue(generator = "idGenerator")
    @Column(name = "id")
    private String id;

    @Column(name = "school_name")
    private String schoolName;

    @OneToMany(mappedBy = "school", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Set<Student> studentList = new HashSet<>();

    @Column(name = "created_dt")
    private Date createdDt;

    @Column(name = "updated_dt")
    private Date updatedDt;

    @Column(name = "is_del")
    private String isDel;
}
@Entity
@Table(name = "tbl_student")
@Data
public class Student {

    @Id
    @GenericGenerator(name = "idGenerator", strategy = "uuid")
    @GeneratedValue(generator = "idGenerator")
    @Column(name = "id")
    private String id;

    @Column(name = "student_name")
    private String studentName;

    @Column(name = "school_id", insertable = false, updatable = false)
    private String schoolId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "school_id")
    private School school;

    @Column(name = "created_dt")
    private Date createdDt;

    @Column(name = "updated_dt")
    private Date updatedDt;

    @Column(name = "is_del")
    private String isDel;

}

基础概念

主键采用UUID策略

    @Id
    @GenericGenerator(name = "idGenerator", strategy = "uuid")
    @GeneratedValue(generator = "idGenerator")
    @Column(name = "id")

Fetch用于关联关系,作用域为读取操作 @OneToMany默认的是FetchType.LAZY(懒加载) @ManyToOne默认的是FetchType.EAGER(急加载)

由于一个School有多个Student,我们可以用@OneToMany去维护这种关系。类似的还有@OneToOne、@ManyToOne,@ManyToMany这些注解。值得注意的话,mappedBy只能适用于@OneToOne,@OneToMany,@ManyToMany这些注解。mappedBy用于主表的一方。对于我们来说School就是主表,Student就是从表。一对多的关系由从表去负责维护。

    @OneToMany(mappedBy = "school", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Set<Student> studentList = new HashSet<>();

再说说与mappedBy互斥的@JoinColumn注解,@JoinColumn用于拥有主表外键的一方,也就是从表。

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "school_id")
    private School school;

mappedBy属性应该指向从表中维护与主表关系的字段。对于School类来说,mappedBy就应该指向Student类中的school属性。

为了让主表知道从表中的那些字段关联自己,在主表一方可以用mappedBy指向从表中的一个关联到自己的对象。在从表一方可以用@JoinColumn注解以外键字段的形式关联到主表。

Cascade用于级联,作用域为增删改操作。CascadeType.ALL包含所有级联策略。(后面会具体演示不同级联策略的效果,加深理解)

public enum CascadeType {

    /** Cascade all operations */
    ALL,

    /** Cascade persist operation */
    PERSIST,

    /** Cascade merge operation */
    MERGE,

    /** Cascade remove operation */
    REMOVE,

    /** Cascade refresh operation */
    REFRESH,

    /**
     * Cascade detach operation
     *
     * @since Java Persistence 2.0
     *
     */
    DETACH
}


toString()方法造成的死循环

我们去查询一个学生,看其否则用了懒加载策略

    @Test
    public void query() {
        Student student = studentDao.findOne("1");
        System.out.println("student=" + student);
    }

结果抛出了这样的异常...

org.hibernate.LazyInitializationException: could not initialize proxy - no Session

	at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:148)
	at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:266)
	at org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.invoke(JavassistLazyInitializer.java:73)
	at cmazxiaoma.model.School_$$_jvstaa_0.toString(School_$$_jvstaa_0.java)

Hibernate跟Spring整合了,Hibernate的Session就交付给Spring去管理。每次数据库操作后,会关闭Session,当我们想要用懒加载方式去获得数据的时候,原来的Session已经关闭,不能获取数据,所以会抛出这样的异常。

我们可以通过Spring提供的OpenSessionInViewFilter去解决这种问题,将Hibernate的Session绑定到整个线程的Servlet过滤器去处理请求,而它必须依赖于Servlet容器,不适用于我们的单元测试。

@Configuration
public class FilterConfig {

    /**
     * 解决hibernate懒加载出现的no session问题
     * @return
     */
//    @Bean
//    public FilterRegistrationBean filterRegistrationBean() {
//        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
//        filterRegistrationBean.setFilter(new OpenSessionInViewFilter());
//        filterRegistrationBean.addInitParameter("urlPatterns", "/*");
//        return filterRegistrationBean;
//    }

    /**
     * 解决jpa 懒加载出现的no session问题
     * @return
     */
    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new OpenEntityManagerInViewFilter());
        filterRegistrationBean.addInitParameter("urlPatterns", "/*");
        return filterRegistrationBean;
    }
}

我们可以在application-dev.properties配置如下代码,就可以在Servlet容器和单元测试中使用懒加载策略了。

#将jpa的session绑定到整个线程的Servlet过滤器,处理请求
spring.jpa.open-in-view=true
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

注意哟,Hibernate依赖SessionFactory去创建Session实例,而JPA依赖于EntityManagerFactory去创建EntityManager实例。


解决了Could not initialize proxy - no session的异常,我们再去跑一下单元测试,出现了更大的错误"StackOverflowError"

java.lang.StackOverflowError
	at org.apache.tomcat.jdbc.pool.ProxyConnection.invoke(ProxyConnection.java:131)
	at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:108)
	at org.apache.tomcat.jdbc.pool.interceptor.AbstractCreateStatementInterceptor.invoke(AbstractCreateStatementInterceptor.java:75)
	at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:108)

我们可以通过日志看到sql的输出,发现了sql重复执行了好多次。以下我截取了前10条sql记录。

Hibernate: select student0_.id as id1_1_0_, student0_.created_dt as created_2_1_0_, student0_.is_del as is_del3_1_0_, student0_.school_id as school_i4_1_0_, student0_.student_name as student_5_1_0_, student0_.updated_dt as updated_6_1_0_ from tbl_student student0_ where student0_.id=?
Hibernate: select school0_.id as id1_0_0_, school0_.created_dt as created_2_0_0_, school0_.is_del as is_del3_0_0_, school0_.school_name as school_n4_0_0_, school0_.updated_dt as updated_5_0_0_ from tbl_school school0_ where school0_.id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select school0_.id as id1_0_0_, school0_.created_dt as created_2_0_0_, school0_.is_del as is_del3_0_0_, school0_.school_name as school_n4_0_0_, school0_.updated_dt as updated_5_0_0_ from tbl_school school0_ where school0_.id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?

通过观察发现,第一条sql是执行查询Student的sql,第二条sql是执行查询School的sql,第三条sql是执行School里面所有学生的sql,第四条sql是执行查询School的sql,后面所有的sql都是执行查询School里面所有学生的sql。

很明显发生了循环依赖的情况。Lombok的@Data相当于@Getter、@Setter、@ToString、@EqualsAndHashCode、@RequiredArgsConstructor注解。

如果我们去掉System.out.println("student=" + student);这行代码,再去跑单元测试,会发现没有报错。

    @Test
    public void query() {
        Student student = studentDao.findOne("1");
        System.out.println("student=" + student);
    }

image.png

我们可以将循环引用的问题定位到Student和School类的toString()方法。Lombok的@Data注解为我们生成的toString()覆盖了整个类的属性。

  // School类
    @Override
    public String toString() {
        return "School{" +
                "id='" + id + '\'' +
                ", schoolName='" + schoolName + '\'' +
                ", studentList=" + studentList +
                ", createdDt=" + createdDt +
                ", updatedDt=" + updatedDt +
                ", isDel='" + isDel + '\'' +
                '}';
    }

   // Student类
    @Override
    public String toString() {
        return "Student{" +
                "id='" + id + '\'' +
                ", studentName='" + studentName + '\'' +
                ", schoolId='" + schoolId + '\'' +
                ", school=" + school +
                ", createdDt=" + createdDt +
                ", updatedDt=" + updatedDt +
                ", isDel='" + isDel + '\'' +
                '}';
    }

我们可以确认System.out.println("student=" + student);会调用Student类中toString()方法,toString()方法会触发school属性的懒加载,便会去调用School类的toString()方法,School()类中的toString()方法,会触发studentList属性的懒加载,接着会调用Student类中的toString()方法。以上就是循环引用的过程。

image.png

我们将@Data注解去掉,换成@Setter、@Getter、@EqualsAndHashCode注解。我们自己重写Student类和School类的toString()方法。

   // School类
    @Override
    public String toString() {
        return "School{" +
                "id='" + id + '\'' +
                ", schoolName='" + schoolName + '\'' +
                '}';
    }

    // Student类
    @Override
    public String toString() {
        return "Student{" +
                "id='" + id + '\'' +
                ", studentName='" + studentName + '\'' +
                '}';
    }

再去跑查询Student的测试用例。

    @Test
    public void query() {
        Student student = studentDao.findOne("1");
        System.out.println("student=" + student);
    }

我们发现输出Student的信息,并没有去查询School的信息。证明懒加载策略起了作用。

Hibernate: select student0_.id as id1_1_0_, student0_.created_dt as created_2_1_0_, student0_.is_del as is_del3_1_0_, student0_.school_id as school_i4_1_0_, student0_.student_name as student_5_1_0_, student0_.updated_dt as updated_6_1_0_ from tbl_student student0_ where student0_.id=?
student=Student{id='1', studentName='卷毛'}

当我们去访问Student的School详情信息时,才会去查询School信息。

    @Test
    public void query() {
        Student student = studentDao.findOne("1");
        System.out.println("student=" + student);

        School school = student.getSchool();
        System.out.println("school=" + school);
    }
Hibernate: select student0_.id as id1_1_0_, student0_.created_dt as created_2_1_0_, student0_.is_del as is_del3_1_0_, student0_.school_id as school_i4_1_0_, student0_.student_name as student_5_1_0_, student0_.updated_dt as updated_6_1_0_ from tbl_student student0_ where student0_.id=?
student=Student{id='1', studentName='卷毛'}
Hibernate: select school0_.id as id1_0_0_, school0_.created_dt as created_2_0_0_, school0_.is_del as is_del3_0_0_, school0_.school_name as school_n4_0_0_, school0_.updated_dt as updated_5_0_0_ from tbl_school school0_ where school0_.id=?
school=School{id='1', schoolName='WE学校'}

hashCode()方法造成的死循环

我们去查询School的信息

    @Test
    public void query() throws Exception {
        School school = schoolDao.findOne("1");
        System.out.println(school);

        Set<Student> studentList = school.getStudentList();
        System.out.println("studentList=" + studentList);
    }

特么,又发现了死循环。我们可以发现执行了查询学校信息的sql,成功输出了学习信息后,才发生死循环。

Hibernate: select school0_.id as id1_0_0_, school0_.created_dt as created_2_0_0_, school0_.is_del as is_del3_0_0_, school0_.school_name as school_n4_0_0_, school0_.updated_dt as updated_5_0_0_ from tbl_school school0_ where school0_.id=?
School{id='1', schoolName='WE学校'}
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select school0_.id as id1_0_0_, school0_.created_dt as created_2_0_0_, school0_.is_del as is_del3_0_0_, school0_.school_name as school_n4_0_0_, school0_.updated_dt as updated_5_0_0_ from tbl_school school0_ where school0_.id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?
Hibernate: select studentlis0_.school_id as school_i4_1_0_, studentlis0_.id as id1_1_0_, studentlis0_.id as id1_1_1_, studentlis0_.created_dt as created_2_1_1_, studentlis0_.is_del as is_del3_1_1_, studentlis0_.school_id as school_i4_1_1_, studentlis0_.student_name as student_5_1_1_, studentlis0_.updated_dt as updated_6_1_1_ from tbl_student studentlis0_ where studentlis0_.school_id=?

通过进一步,看到栈异常的错误定位在School类和Student类中的hashCode()。

java.lang.StackOverflowError
	at cmazxiaoma.model.School.hashCode(School.java:22)
	at sun.reflect.GeneratedMethodAccessor38.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.invoke(JavassistLazyInitializer.java:84)
	at cmazxiaoma.model.School_$$_jvstc33_0.hashCode(School_$$_jvstc33_0.java)
	at cmazxiaoma.model.Student.hashCode(Student.java:20)

那Student和School类中的hashCode()还在什么情况下调用呢? studentList是Set集合,HashSet内部实现其实是通过HashMap,HashSet的元素其实就是内部HashMap的key,HashMap的key不能重复决定了HashSet的元素不能重复。我们往HashSet里面添加元素时,其实会调用hashCode()和equals()确定元素在HashMap存储的具体位置。

    @OneToMany(mappedBy = "school", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Set<Student> studentList = new HashSet<>();

通过反编译School类和Student类,我们发现它们的hashCode()方法存在循环引用。 看School类中的hashCode()方法,studentList是一个HashSet集合,HashSet集合的hashCode()计算方式会遍历所有元素,累加求和每个元素的hashCode值。但是studentList里面元素的类型是Student,Student类中的hashCode()又会依赖于School类的hashCode()方法,这样就形成了循环依赖。

    // School类的hashCode()方法
    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $id = this.getId();
        int result = result * 59 + ($id == null ? 43 : $id.hashCode());
        Object $schoolName = this.getSchoolName();
        result = result * 59 + ($schoolName == null ? 43 : $schoolName.hashCode());
        Object $studentList = this.getStudentList();
        result = result * 59 + ($studentList == null ? 43 : $studentList.hashCode());
        Object $createdDt = this.getCreatedDt();
        result = result * 59 + ($createdDt == null ? 43 : $createdDt.hashCode());
        Object $updatedDt = this.getUpdatedDt();
        result = result * 59 + ($updatedDt == null ? 43 : $updatedDt.hashCode());
        Object $isDel = this.getIsDel();
        result = result * 59 + ($isDel == null ? 43 : $isDel.hashCode());
        return result;
    }
     
   // Student类中的hashCode()方法
    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $id = this.getId();
        int result = result * 59 + ($id == null ? 43 : $id.hashCode());
        Object $studentName = this.getStudentName();
        result = result * 59 + ($studentName == null ? 43 : $studentName.hashCode());
        Object $schoolId = this.getSchoolId();
        result = result * 59 + ($schoolId == null ? 43 : $schoolId.hashCode());
        Object $school = this.getSchool();
        result = result * 59 + ($school == null ? 43 : $school.hashCode());
        Object $createdDt = this.getCreatedDt();
        result = result * 59 + ($createdDt == null ? 43 : $createdDt.hashCode());
        Object $updatedDt = this.getUpdatedDt();
        result = result * 59 + ($updatedDt == null ? 43 : $updatedDt.hashCode());
        Object $isDel = this.getIsDel();
        result = result * 59 + ($isDel == null ? 43 : $isDel.hashCode());
        return result;
    }

HashSet的hashCode()方法来自与父类AbstractSet。

    public int hashCode() {
        int h = 0;
        Iterator<E> i = iterator();
        while (i.hasNext()) {
            E obj = i.next();
            if (obj != null)
                h += obj.hashCode();
        }
        return h;
    }

既然发现了是@Data注解生成的hashCode()方法坑了我们,那我们自己重写Student和Teacher类中的hashCode()和equals()方法

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof School)) return false;
        if (!super.equals(o)) return false;

        School school = (School) o;

        if (!getId().equals(school.getId())) return false;
        if (!getSchoolName().equals(school.getSchoolName())) return false;
        if (!getCreatedDt().equals(school.getCreatedDt())) return false;
        if (!getUpdatedDt().equals(school.getUpdatedDt())) return false;
        return getIsDel().equals(school.getIsDel());
    }

    @Override
    public int hashCode() {
        int result = super.hashCode();
        result = 31 * result + getId().hashCode();
        result = 31 * result + getSchoolName().hashCode();
        result = 31 * result + getCreatedDt().hashCode();
        result = 31 * result + getUpdatedDt().hashCode();
        result = 31 * result + getIsDel().hashCode();
        return result;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Student)) return false;

        Student student = (Student) o;

        if (!getId().equals(student.getId())) return false;
        if (!getStudentName().equals(student.getStudentName())) return false;
        if (!getSchoolId().equals(student.getSchoolId())) return false;
        if (!getCreatedDt().equals(student.getCreatedDt())) return false;
        if (!getUpdatedDt().equals(student.getUpdatedDt())) return false;
        return getIsDel().equals(student.getIsDel());
    }

    @Override
    public int hashCode() {
        int result = getId().hashCode();
        result = 31 * result + getStudentName().hashCode();
        result = 31 * result + getSchoolId().hashCode();
        result = 31 * result + getCreatedDt().hashCode();
        result = 31 * result + getUpdatedDt().hashCode();
        result = 31 * result + getIsDel().hashCode();
        return result;
    }

记住我们重写equals()方法,就必须要重写hashCode()方法。可以看到Student类和School类都有id、createdDt、updatedDt、isDel的属性,我们如果把这些相同属性都提到父类中,让Student类和School类继承这个父类,同时使用@EqualsAndHashCode注解为其生成equals()和hashCode()方法。那么会出现一个问题,在比较对象是否相等时会得出错误的结果。因为@EqualsAndHashCode生成的equals()和hashCode()没有使用父类的属性。接下来,我们就测试一下吧。


@EqualsAndHashCode的坑

定义一个Father类。

@Getter
@Setter
@EqualsAndHashCode
public class Son extends Father {

    private String sonName;

}

定义一个Son类。

@Getter
@Setter
@EqualsAndHashCode
public class Son extends Father {

    private String sonName;

}

我们运行下面的代码,比较son1和son2对象是否相等。结果返回true,很显然只比较Son对象的属性,没有比较Son的父类Father里面的属性。

public class SonTest {

    @Test
    public void test() {
        Son son1 = new Son();
        son1.setSonName("son1");
        son1.setFatherName("baseFather");

        Son son2 = new Son();
        son2.setSonName("son1");
        son2.setFatherName("baseFather2");

        System.out.println(son1.equals(son2));

    }
}

image.png

查看反编译后的Son类代码,恍然大悟。

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof Son)) {
            return false;
        } else {
            Son other = (Son)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                Object this$sonName = this.getSonName();
                Object other$sonName = other.getSonName();
                if (this$sonName == null) {
                    if (other$sonName != null) {
                        return false;
                    }
                } else if (!this$sonName.equals(other$sonName)) {
                    return false;
                }

                return true;
            }
        }
    }

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

项目地址

会陆续更新使用Hibernate、Mybatis、JPA碰到的有趣问题,会打算从源码角度分析MyBatis


刚才看了评论,顺便再提一下。Lombok的@EqualsAndHashCode生成的equals()和hashCode()默认是不调用父类的实现。 设置其属性callSuper为true时,就可以了。

	/**
	 * Call on the superclass's implementations of {@code equals} and {@code hashCode} before calculating
	 * for the fields in this class.
	 * <strong>default: false</strong>
	 */
	boolean callSuper() default false;

equals.png

hashcode.png

尾言

在没有真正理解框架干了什么之前,不要对框架充分信任。我们要明白Lombok框架干了什么,不然出现一堆问题就懵逼了。