Spring boot請求參數數據綁定重載set方法衝突異常原因解析

最近在工作中遇到了這樣一個問題:一個後端接口,請求對象中有一個字段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中方法重載不起作用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章