在java專案中的mongodb的_id被fastjson轉為json時竟然丟失了

親吻昨日的陽光發表於2015-09-09

fastjson是阿里開發的一個javaBean和json解析器和封裝器(原始碼位置),用過幾次感覺挺好用的,也是國人的開源專案當然得支援,但最近專案在使用mongodb作為資料庫時出現了_id丟失的問題,現將我遇到的問題和解決辦法展示一下。

現將錯誤的程式程式碼新增上,然後再提供解決方法:

package org.jivesoftware.openfire.plugin.friends.test;

import org.bson.types.ObjectId;
import org.jivesoftware.openfire.plugin.friends.util.JsonUtil;

import com.alibaba.fastjson.annotation.JSONField;

/**
 * @author Administrator
 *
 */
public class TestJson {

    private String id;

    private String __id;

    private ObjectId _id;



    public String get__id() {
        return __id;
    }

    public void set__id(String __id) {
        this.__id = __id;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    //@JSONField(serialize=false,deserialize=false)
    public ObjectId get_id() {
        return _id;
    }

    public void set_id(ObjectId _id) {
        this._id = _id;
    }



    @Override
    public String toString() {
        return "TestJson [id=" + id + ", __id=" + __id + ", _id=" + _id + "]";
    }

    public static void main(String[] args) throws Exception {
        TestJson testJson = new TestJson();

        testJson.setId("1234567890987654");
        testJson.set__id("abcdefghijklmnopqrst");
        String json = JSON.toJSONString(testJson);
        System.out.println("json:"+json);
        TestJson tj =(TestJson)JSON.parseObject(json,TestJson.class);
        System.out.println(tj.toString());
    }
}

當然要先把fastjson和mongodb的jar包匯入專案。
執行後的結果如下:

json:{"_id":"abcdefghijklmnopqrst"}
TestJson [id=null, __id=abcdefghijklmnopqrst, _id=null]
有沒有發現屬性id的值沒有被轉為JSON?
這是在專案開發中困擾了有一段時間的問題,因為開發者需要將id作為引數傳遞給伺服器做刪改查操作。而沒有了id,我怎麼去操作。工作一時陷入停頓。
沒辦法,在網上沒找到解決辦法,就只能去除錯跟蹤fastjson原始碼,最終發現問題所在,貼出問題出現的fastjson的原始碼片段:
//程式碼片段1
 char c3 = methodName.charAt(3);

 String propertyName;
  if (Character.isUpperCase(c3)) {
      if (compatibleWithJavaBean) {
          propertyName = decapitalize(methodName.substring(3));
      } else {
          propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
      }
  } else if (c3 == '_') {
      propertyName = methodName.substring(4);
  } else if (c3 == 'f') {
      propertyName = methodName.substring(3);
  } else if (methodName.length()>=5 && Character.isUpperCase(methodName.charAt(4))){
      propertyName = decapitalize(methodName.substring(3));
  } else {
      continue;
  }

該程式碼片段位置:src/main/java/com/alibaba/fastjson/util/TypeUtils.java中。
現在解釋下我跟蹤到的根源:
看這段程式碼

else if (c3 == '_') {
      propertyName = methodName.substring(4);
  } 

這句話就是導致_id丟失或者說id屬性值丟失的原因所在,fastjson獲得JavaBean物件屬性作為json中key的方式是通過擷取JavaBean中屬性的getter方法名得到的,而c3是獲取的方法名的index=3,也就是第四個字母。而因為我的getter和setter方法都是通過Eclipse快捷生成的,因此getter方法名get_id的第四個位置是下劃線,而當該位置是下劃線時,預設獲取的屬性名(也就是_id轉為json中的key)是從下一個位置開始的,最終propertyName=”id”.

追蹤原始碼:

 List<FieldInfo> getters = TypeUtils.computeGetters(clazz, aliasMap, false);

除錯結果:

[id, _id]

將三個屬性的除錯資訊顯示在下面:
1.屬性id的除錯資訊,存在fieldInfoMap中
這裡寫圖片描述

2.屬性__id(雙下劃線)的除錯資訊
這裡寫圖片描述

3.屬性_id(單下劃線)的除錯資訊
這裡寫圖片描述

重點是id和_id,由上面的除錯資訊可以看到屬性id的map資訊是最先存入的,最後存入的_id,但兩者的key都是id,因此_id就將id的map資訊給覆蓋掉了,因此,也就沒有了屬性id對應的map值。
fieldInfoMap只是暫存資訊,需要返回給呼叫方法使用的資訊儲存在fieldInfoList 中,

  List<FieldInfo> fieldInfoList = new ArrayList<FieldInfo>();
  if (containsAll) {
      for (String item : orders) {
          FieldInfo fieldInfo = fieldInfoMap.get(item);
          fieldInfoList.add(fieldInfo);
      }
  } else {
      for (FieldInfo fieldInfo : fieldInfoMap.values()) {
          fieldInfoList.add(fieldInfo);
      }

      if (sorted) {
          Collections.sort(fieldInfoList);
      }
  }

執行的程式碼部分:
這裡寫圖片描述
最後返回的fieldInfoList 截圖如下:
這裡寫圖片描述

到了這裡應該就大體明白了,fastjson是通過擷取get方法的方式來獲取屬性名的,而在一般情況下,getter和setter方法通過Eclipse快捷生成,並且在屬性中沒有下劃線開頭的屬性的時候,程式碼片段1處的方法執行是沒有錯誤的,但錯就錯在如果屬性以下劃線開頭,那麼生成的json中key和JavaBean中屬性就不能對應了,很容易造成本例中的誤解,誤認為是__id(雙下劃線),沒有解析成功。

既然錯誤的根源找到了,那我們就需要提供一些辦法來解決掉這些問題。

1.不使用fastjson去解析,這是一種方法,但也是在逃避fastjson的問題。
2.將_id和id賦值相同(_id->面向mongodb,id->面向開發者),那麼即使id屬性被覆蓋,但json中的值還是能獲取到的,只不過是被相同的值替代。
3.使用fastjson註解方式實現:

這裡重點說一下第三種方法:

fastjson提供了註解的方式可以為指定的屬性設定要序列化的名稱,需要配置在setter和getter方法上。
測試過的方式有三種:
(1).在類名稱上新增忽略物件註解,如@JSONType(ignores="_id")。是不是覺得這樣就能在解析為json時將屬性_id的影響消除?接著往下看
 boolean ignore = isJSONTypeIgnore(clazz, propertyName);
 if (ignore) {
      continue;
  }

這句程式碼是在獲得屬性propertyName之後,然後判斷與註解中忽略的_id是否匹配,如果ignore=false;則將屬性加入到fieldInfoMap中,否則直接進行下一次迴圈。

返回值就如這樣:

json:{}
TestJson [id=null, __id=null, _id=null]

因為在構建fieldInfoMap時,__id(雙下劃線)對應的_id未被加入到fieldInfoMap中,而_id對應的id的值為空,因此生成的json就只是空物件。因此該註解也不能很好的解決該問題。

(2).在屬性_id上新增註解,如@JSONField(serialize=false,deserialize=false)

@JSONField(serialize=false,deserialize=false)
private ObjectId _id;

貌似根據以前使用註解,這樣子定義在屬性上的不可被序列化和反序列化的註解應該可以正常使用了,但本例子又跟您開了個玩笑,即結果跟方法1的一樣。

json:{}
TestJson [id=null, __id=null, _id=null]

原因就出現下面的程式碼中:

Field field = ParserConfig.getField(clazz, propertyName);
JSONField fieldAnnotation = field.getAnnotation(JSONField.class);
if (fieldAnnotation != null) {
    if (!fieldAnnotation.serialize()) {
        continue;
}

該程式碼也是在解析了屬性名稱之後去被獲取註解的屬性,因此也就會造成該註解對應的屬性比實際的屬性少一個下劃線”_”(如果屬性確實以下劃線開始),那就會造成開發中被註解的屬性依然序列化,而未被註解的屬性卻未被序列化。

要麼這就是阿里在開發該解析器時對應用場景考慮不周,要麼就是阿里開發人員預設認為使用者不會定義以下劃線開頭的屬性(不過要真是這個原因,那我也無話可說了)。

(3).在屬性_id的get方法上新增註解,如@JSONField(serialize=false,deserialize=false),該註解和方法2中的註解相同,不同之處就是註釋的位置不同而已。

@JSONField(serialize=false,deserialize=false)
public ObjectId get_id() {
    return _id;
}

這樣得出的結果是部分正確的(也就是錯誤的):

json:{"_id":"abcdefghijklmnopqrst","id":"1234567890987654"}
TestJson [id=1234567890987654, __id=abcdefghijklmnopqrst, _id=null]

因為序列化為json時屬性名稱還是錯誤的,但反序列化為java物件時是正確的。

關鍵程式碼如下:

JSONField annotation = method.getAnnotation(JSONField.class);
if (annotation == null) {
    annotation = getSupperMethodAnnotation(clazz, method);
}

if (annotation != null) {
    if (!annotation.serialize()) {
        continue;
    }
    ...
}

這句話是檢測方法上的JSONField註解,如果有該註解,並且annotation.serialize()==false。則認為該方法及其對應的屬性不被序列化,因此就執行continue,進入下一次迴圈,也就避免了id的map被_id的map覆蓋的悲劇。同時也就能正確的將有非空值的屬性轉為json物件。雖然結果是正確的,但JavaBean中以下劃線開頭的屬性對應的json中的key依然缺少一個下劃線。

(4).終極解決,在屬性的setter和getter方法上使用註解@JSONField(name="_id"),為其指定要序列化和反序列化後的屬性名,getter是序列化為json,setter是反序列化為java物件。

以上是使用fastjson解決該問題,暫時得出結論是像類似同一JavaBean中有”_id”和”id”這樣的屬性,就只能使用在get方法上新增JSONField註解的方式解決。

還有其他撇開fastjson的方法:
(1).自己組串,不使用JavaBean,而直接寫成json格式的字串;
(2).改屬性名,不使用類似”_id”和”id”這樣的屬性。
不知道為什麼專案中會出現如此奇葩的定義,不過既然這樣定義了,也使用了。還是做下記錄比較好,也省的自己以後可能再做如此的重複工作。
(3).不為_id設定get方法,也可以獲得正確的json串。但在解析為JavaBean物件時會報錯。

心得:發現在使用開源框架進行開發時,如果網上沒有很好的解決辦法,跟蹤閱讀程式原始碼是個一舉多得的好辦法,即能深入瞭解問題根源,又能學習優秀原始碼,還能提高自己處理問題的能力。

相關文章