解決spring data jpa 一對多做粤,多對一雙向依賴引用遞歸浇借,查詢出現(xiàn)java.lang.StackOverflowError: null問題

我們在開發(fā)項目中,會經(jīng)常根據(jù)不同的業(yè)務(wù)設(shè)計出不同的實體關(guān)聯(lián)關(guān)系表怕品,用到的最多的就是一對多妇垢,多對一,大部分用到的都是單向關(guān)聯(lián)肉康。在這里闯估,我們要解決雙向關(guān)聯(lián)查詢數(shù)據(jù)出現(xiàn)死循環(huán)、棧溢出的問題吼和。

我就用項目中的實體關(guān)系(表)舉例說明了:

定義兩張表(task_info,track_info)分別對應(yīng)的實體類為:TaskInfo涨薪、TrackInfo,它們的關(guān)系是一對多炫乓,多對一雙向關(guān)聯(lián)刚夺,

一個任務(wù)中有多個任務(wù)的追蹤信息献丑,同時追蹤信息中又能根據(jù)外鍵關(guān)聯(lián)到任務(wù)的信息

@Entity
@EntityListeners(AuditingEntityListener.class)
@Data
@Table(name ="scrm_clue_task_info")
public class TaskInfo {
  /**
    * 主鍵Id,自動生成
    */
    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;

    @OneToMany(mappedBy ="taskInfo")
     private List<TrackInfo> trackInfoList;
  }
@Entity
@Data
@EntityListeners(AuditingEntityListener.class)
@Table(name = "scrm_clue_track_info")
public class TrackInfo {

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

    /**
     * 若不指定@JoinColumn,默認(rèn)會生成:表名_id的外鍵字段
     */
    @ManyToOne(fetch = FetchType.LAZY)
    private TaskInfo taskInfo;
  }

定義Service層侠姑,查詢taskInfo方法

@Slf4j
@Service
public class TaskManageServiceImpl implements TaskManageService {

    @Autowired
    private TaskInfoRepository taskInfoRepository;

   /**
     * 根據(jù)id查詢?nèi)蝿?wù)詳情
     *
     * @param id 任務(wù)id
     * @return 任務(wù)詳情信息
     */
    @Override
    public TaskInfo findTaskDetail(String taskInfoId) {
        TaskInfo taskInfo = taskInfoRepository.getOne(taskInfoId);
        return taskInfo;
    }
}

在上面的service代碼中创橄,正常情況下通過findTaskDetail()方法可以根據(jù)任務(wù)的id(taskInfoId)查詢到任務(wù)的基本信息以及關(guān)聯(lián)到的任務(wù)追蹤信息(trackInfoList)。但是由于我們之前設(shè)計的是雙向關(guān)聯(lián)關(guān)系莽红,在調(diào)用查詢方法的時候hibernate將結(jié)果查詢出來并會調(diào)用set妥畏、get、tostring方法來序列化對象船老,會出現(xiàn)無限遞歸循環(huán)導(dǎo)致的tostring()堆棧溢出的錯誤:

Method threw 'java.lang.StackOverflowError' exception. Cannot evaluate com.markor.scrm.clue.entity.TaskInfo_$$_jvst491_7.toString()

這個問題是因為在實體類上標(biāo)注的lombok的@Data注解導(dǎo)致的咖熟,簡單來說@Data注解會自動幫我們實現(xiàn)get、set柳畔、hashcode馍管、equals、toString等方法薪韩。我們來看一下TaskInfo.class反編譯中@Data注解自動幫我們實現(xiàn)的toString()方法:

public String toString() {
        return "TaskInfo(id=" + this.getId()
                + "trackInfoList=" + this.getTrackInfoList() + ")";
    }

看完上面的代碼相信大家一定知道了确沸,在查詢到對象進行賦值的時候,會調(diào)用每個屬性的toString()方法:

toString堆棧溢出報錯信息
調(diào)用toString方法

在調(diào)用toString()方法的時候會從taskInfo對象中獲得trackInfoList俘陷,trackinfoList中又獲得taskInfo罗捎,從而一直無限遞歸下去導(dǎo)致棧溢出。

解決方法:將@Data注解換成@getter拉盾、@setter方法桨菜,不讓它幫我們自動重寫toString()方法,或者自己覆蓋掉toString()方法捉偏。

上面說了在hibernate查詢對象序列化的時候倒得,會對對象中每個屬性進行g(shù)et、set賦值夭禽。實際上在返回到接口調(diào)用到結(jié)果的過程中霞掺,spring會通過HttpMessageConverter<T>來實現(xiàn)將對象JSON序列化返回給前端。
這個時候我們將對象序列化的時候讹躯,要避免對象之間遞歸重復(fù)引用調(diào)用的坑菩彬!

以下我列舉解決三個方案來規(guī)避返回前端序列化堆棧溢出的問題:

方法一:
如果你是使用spring jar包中自帶的Jackson2JsonMessageConverter轉(zhuǎn)換器
在屬性上加入@JsonIgnoreProperties注解:此注解的意思是會忽略對象中的taskInfo屬性,在這里要注意:是trackInfoList中的taskInfo屬性:

@JsonIgnoreProperties(value = {"taskInfo"})
@OneToMany(mappedBy = "taskInfo")
private List<TrackInfo> trackInfoList

如果你在前臺需要返回另一方的結(jié)果集潮梯,也需要加上此注解:
@JsonIgnoreProperties(value = {"taskInfo"})
@ManyToOne(fetch = FetchType.LAZY)
private TaskInfo taskInfo;
這樣的話得出的結(jié)果是taskInfo對象中有trackInfoList骗灶,而trackInfoList中不會對taskInfo重復(fù)引用,我們看一下結(jié)果:

{
    "code": "0000",
    "data": {
        "id":"123",
        "trackInfoList": [
            {
                "createTime": "2020-02-16 17:56:15",
                "customerFeedBackInformation": "再激活",
                "deterMineEnterStore": "2020-02-11 17:42:52",
                "id": "8a85c6ca704d678401704d6d51230002",
                "isHaveWillingness": "1",
                "isWillAnswer": "1",
                "nextTrackingDate": "2020-03-16 17:56:12",
                "operatorName": "張亞楠",
                "operatorNumber": "0117498",
                "operatorPost": "大自然的搬運工",
                "taskLevel": "A"
            },
            {
                "createTime": "2020-02-14 17:34:03",
                "customerFeedBackInformation": "121",
                "deterMineEnterStore": "2020-02-11 17:42:52",
                "id": "8aaa43887042a17f0170430c479a0004",
                "isHaveWillingness": "1",
                "isWillAnswer": "1",
                "nextTrackingDate": "2020-02-20 17:42:52",
                "operatorName": "大自然的搬運工",
                "operatorNumber": "0117498",
                "taskLevel": "A"
            }
        ],
    },
    "message": "",
    "searchTime": 1581857081314,
    "success": true
}

大家可能會說秉馏,為什么不在屬性上使用@JsonIgnore注解矿卑?
在這里我要解釋一下,此注解是忽略屬性序列化沃饶,實際上就是Transient的意思母廷,在哪個屬性上面加,哪個屬性就不會被序列化糊肤。如果在TaskInfo類中的trackInfoList屬性上面加入@JsonIgnore琴昆,會導(dǎo)致返回的結(jié)果trackInfoList沒有被序列化,trackInfoList結(jié)果為空馆揉,顯然业舍,這不是我們想要的結(jié)果。

方法二:

有兩種方式
如果項目中使用FastJson來實現(xiàn)HttpMessageConverter<T>轉(zhuǎn)換器升酣,spring在序列化對象的時候會優(yōu)先采用自己實現(xiàn)的序列化方案舷暮,所以調(diào)用序列化write方法會采用你自己實現(xiàn)的FastJson,而不是spring默認(rèn)的Jackson2JsonMessageConverter轉(zhuǎn)換器的方法噩茄。
所以在這里如果要用@JsonIgnoreProperties注解就沒有作用了下面,因為此注解是package com.fasterxml.jackson.annotation包下的,fastjson是不會解析到的绩聘。
可以使用fastjson包下的@JSONField注解沥割,這樣可以序列化trackList屬性的時候忽略“taskInfo”屬性:
(1)在多的一方TrackInfo類中taskInfo屬性加上@JSONField

@ManyToOne(cascade = {CascadeType.ALL}, fetch = FetchType.LAZY)
@JSONField(serialize = false)
 private TaskInfo taskInfo;

(2)需要自己定制序列化方法:

@JSONField(serializeUsing = CusSerializer.class)
private List<TrackInfo> trackInfoList;

serializeUsing 的意思是使用自己定制的序列化方法,如果不填的話凿菩,它會默認(rèn)一個類机杜。
要定制序列化類,我們要實現(xiàn)ObjectSerializer類衅谷,實現(xiàn)write()方法:

public class CusSerializer implements ObjectSerializer {
    @Override
    public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
        System.out.println("******進入CusSerializer序列化*******");
        //注意:一定不要直接操作對象
        //((List<TrackInfo>)object).forEach(o -> o.setTaskInfo(null));
        //使用copy對象的方法來忽略taskInfo屬性椒拗,再去序列化
        List<TrackInfo> trackInfoList = BeanHelper.copyWithCollection(((List<TrackInfo>) object), TrackInfo.class, "taskInfo");
        serializer.write(object);
    }
}

根據(jù)上面的代碼,有的小伙伴們一定會對被注釋掉直接操作對象的那行代碼有所疑問获黔,為什么不能使用呢蚀苛?
因為:如果你在service類中調(diào)用了序列化的方法(很有可能是將對象序列化成字節(jié),發(fā)送mq)肢执,此時對象為Persistent(持久化狀態(tài))枉阵,service層在提交事務(wù)的時候,會發(fā)現(xiàn)屬性有改變预茄,執(zhí)行update語句進行更新兴溜,這時trackInfo中關(guān)聯(lián)的外鍵task_info_id就被更新沒了,數(shù)據(jù)會有問題耻陕。

在屬性中定義自己實現(xiàn)的序列化方法拙徽,該屬性就會調(diào)用此序列化方法的策略,進行序列化诗宣,結(jié)果和上圖效果也是一樣的膘怕。

方法三:

筆者還有一種更簡單粗暴的方法,在TaskInfo中重寫getTrackInfoList()方法召庞,在方法中去除重復(fù)引用岛心。此方法是在返回給前端序列化之前来破,就已經(jīng)執(zhí)行了。換句話說:在執(zhí)行完taskInfoRepository.getOne(taskInfoId);方法的時候就已經(jīng)賦值調(diào)用完成了忘古,代碼如下:

public List<TrackInfo> getTrackInfoList() {
       System.out.println("********************進入getTrackInfoList方法******************");
       if (!CollectionUtils.isEmpty(this.trackInfoList)) {
           return BeanHelper.copyWithCollection(this.trackInfoList,TrackInfo.class,"taskInfo");
        
    }
    return trackInfoList;

以上內(nèi)容希望對遇到此問題的小伙伴們有幫助徘禁,如有何問題,請在留言板留言髓堪,歡迎一起討論~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載送朱,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末干旁,一起剝皮案震驚了整個濱河市驶沼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌争群,老刑警劉巖回怜,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異祭阀,居然都是意外死亡鹉戚,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門专控,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抹凳,“玉大人,你說我怎么就攤上這事伦腐∮祝” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵柏蘑,是天一觀的道長幸冻。 經(jīng)常有香客問我,道長咳焚,這世上最難降的妖魔是什么洽损? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮革半,結(jié)果婚禮上碑定,老公的妹妹穿的比我還像新娘。我一直安慰自己又官,他們只是感情好延刘,可當(dāng)我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著六敬,像睡著了一般碘赖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天普泡,我揣著相機與錄音播掷,去河邊找鬼。 笑死劫哼,一個胖子當(dāng)著我的面吹牛叮趴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播权烧,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼伤溉!你這毒婦竟也來了般码?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤乱顾,失蹤者是張志新(化名)和其女友劉穎板祝,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體走净,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡券时,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了伏伯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片橘洞。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖说搅,靈堂內(nèi)的尸體忽然破棺而出炸枣,到底是詐尸還是另有隱情,我是刑警寧澤弄唧,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布适肠,位于F島的核電站,受9級特大地震影響候引,放射性物質(zhì)發(fā)生泄漏侯养。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一澄干、第九天 我趴在偏房一處隱蔽的房頂上張望逛揩。 院中可真熱鬧,春花似錦傻寂、人聲如沸息尺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搂誉。三九已至,卻和暖如春静檬,著一層夾襖步出監(jiān)牢的瞬間炭懊,已是汗流浹背并级。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留侮腹,地道東北人嘲碧。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像父阻,于是被迫代替她去往敵國和親愈涩。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容