使用Hibernate、JPA、Lombok遇到的有趣問題

markriver發表於2021-09-09

前言

先用我不是藥神電影海報鎮樓,這個電影真心不錯,推薦大家。


圖片描述

image.png

準備

講解Hibernate之前,首先建立兩個實體類,一個是Student類,一個School類。School和Student的關係是一對多的關係

@Entity@Table(name = "tbl_school")@Datapublic 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 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")@Datapublic 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 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容器,不適用於我們的單元測試。

@Configurationpublic 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=truespring.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 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 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 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@EqualsAndHashCodepublic class Son extends Father {    private String sonName;

}

定義一個Son類。

@Getter@Setter@EqualsAndHashCodepublic 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;
    }

專案地址

尾言

在沒有真正理解框架幹了什麼之前,不要對框架充分信任。我們要明白Lombok框架幹了什麼,不然出現一堆問題就懵逼了。



作者:cmazxiaoma
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/818/viewspace-2810372/,如需轉載,請註明出處,否則將追究法律責任。

相關文章