我們在開發(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()方法:
在調(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;