最近在工作中遇到了這樣一個問題:一個後端接口,請求對象中有一個字段List<Integer> status,有兩個地方調用該接口,其中一個傳參status:[1,2],其中一個傳參status:1。前一個接口調用沒問題,後一個接口調用報錯,因爲類型不匹配。最開始,出於不需要前端頁面同步修改考慮,直接對對象中的status字段進行了set方法重載,如下:
然而實際運行中卻報錯了:
本地測試的時候發現這個報錯上面有兩行warn告警信息:
基本可以判斷是jackson在數據綁定的時候由於有多個set方法造成了衝突,經驗證的確如此。
那麼,爲什麼呢?(spring boot version:2.0.6.RELEASE, jackson version: 2.9.9)
爲什麼Spring boot在進行數據自動綁定的時候不支持set方法重載?爲什麼Jackson在進行數據綁定的時候對於重載set方法會報衝突?fastjson有這個問題嗎?
帶着上面這些疑問,我簡單的寫了個demo進行調試,追蹤上述異常出現的原因。
上述問題實際上就是jackson進行對象反序列化時set方法衝突問題,簡單處理,直接寫一個main方法進行調試:
直接運行,報錯:
進入POJOPropertyBuilder.java,發現是getSetter()方法拋出的異常。閱讀getSetter()源碼,
public AnnotatedMethod getSetter() {
POJOPropertyBuilder.Linked<AnnotatedMethod> curr = this._setters;
if (curr == null) {
return null;
} else {
POJOPropertyBuilder.Linked<AnnotatedMethod> next = curr.next;
A if (next == null) {
return (AnnotatedMethod)curr.value;
B } else {
while(true) {
if (next == null) {
this._setters = curr.withoutNext();
return (AnnotatedMethod)curr.value;
}
label42: {
Class<?> currClass = ((AnnotatedMethod)curr.value).getDeclaringClass();
Class<?> nextClass = ((AnnotatedMethod)next.value).getDeclaringClass();
if (currClass != nextClass) {
if (currClass.isAssignableFrom(nextClass)) {
curr = next;
break label42;
}
if (nextClass.isAssignableFrom(currClass)) {
break label42;
}
}
AnnotatedMethod nextM = (AnnotatedMethod)next.value;
AnnotatedMethod currM = (AnnotatedMethod)curr.value;
int priNext = this._setterPriority(nextM);
int priCurr = this._setterPriority(currM);
if (priNext != priCurr) {
if (priNext < priCurr) {
curr = next;
}
} else {
if (this._annotationIntrospector == null) {
break;
}
C AnnotatedMethod pref = this._annotationIntrospector.resolveSetterConflict(this._config, currM, nextM);
D if (pref != currM) {
if (pref != nextM) {
break;
}
curr = next;
}
}
}
next = next.next;
}
throw new IllegalArgumentException(String.format("Conflicting setter definitions for property \"%s\": %s vs %s", this.getName(), ((AnnotatedMethod)curr.value).getFullName(), ((AnnotatedMethod)next.value).getFullName()));
}
}
}
發現getSetter()的作用就是確認某個參數對應的set方法,如果有多個,會進行衝突處理。如圖,行A代表只有一個set方法,直接返
回,行B進行多個set方法處理---這裏可以看出,Jackson是支持多set方法的,重點方法在行C:
AnnotatedMethod pref = this._annotationIntrospector.resolveSetterConflict(this._config, currM, nextM);
public AnnotatedMethod resolveSetterConflict(MapperConfig<?> config, AnnotatedMethod setter1, AnnotatedMethod setter2) {
Class<?> cls1 = setter1.getRawParameterType(0);
Class<?> cls2 = setter2.getRawParameterType(0);
if (cls1.isPrimitive()) {
if (!cls2.isPrimitive()) {
return setter1;
}
} else if (cls2.isPrimitive()) {
return setter2;
}
if (cls1 == String.class) {
if (cls2 != String.class) {
return setter1;
}
} else if (cls2 == String.class) {
return setter2;
}
return null;
}
從上面方法可以看出,Jackson在進行set方法衝突解決的時候是根據方法請求參數的類型是否基本類型以及String類型來選擇set方法,和具體的待綁定的請求數據的類型無關。到這裏,基本可以知道例子中異常的原因:Integer 和 List都不是基本數據類型,因此該方法返回null,回到getSetter()方法中,D行,pref != currM && pref != nextM, 退出循環,拋出異常。
有意思的是,這裏的_setters中拿到的set方法的順序是不固定的,因此假設再增加一個setStatus(String s)方法,多次調用,有可能會成功。這點有興趣的同學可以自己嘗試一下。另外,子類的情況大家也可以嘗試一下。
針對這種情況,如果切實有需要重載set方法的需求,可以考慮使用泛型:
private List<Integer> status;
public <T> void setStatus(T object){
if(object instanceof String){
this.status = Arrays.asList(((String)object).split(",")).stream().map(Integer::parseInt).collect(Collectors.toList());
}else if(object instanceof List){
status = (List)object;
}else if(object instanceof Integer){
status = Arrays.asList((Integer) object);
}
}
這裏還有一個疑問,Jackson處理衝突的時候爲什麼不根據反序列化的數據的類型進行set方法的匹配?
最後,fastjson中方法重載不起作用。