背景
為了在日志中把出入參打印出來,以便驗證鏈路和排查問題,在日志中將入參用fastjson格式化成字符串輸出,結果遇到了NPE。
問題復現
示例代碼
public static void main(String[] args) {OrganizationId orgId = new OrganizationId();NodeName name = new NodeName("test");Node node = new Node();node.setName(name);node.setOrganizationId(orgId);System.out.println(JSONObject.toJSONString(node));
}
錯誤提示
發現是OrganizationId對象里的方法報空指針了,趕緊看一眼這個類:
public class OrganizationId {private String id;public Long getIdToLong() {return Long.valueOf(this.id);}
}
怎么會運行到 getIdToLong 方法呢?
問題排查
對 JSONObject.toJSONString 方法進行反復 debug 之后,終于發現了原因,以下是具體路徑:
public static String toJSONString(Object object, SerializeConfig config, SerializeFilter[] filters, String dateFormat,int defaultFeatures, SerializerFeature... features) {SerializeWriter out = new SerializeWriter(null, defaultFeatures, features);try {JSONSerializer serializer = new JSONSerializer(out, config);if (dateFormat != null && dateFormat.length() != 0) {serializer.setDateFormat(dateFormat);serializer.config(SerializerFeature.WriteDateUseDateFormat, true);}if (filters != null) {for (SerializeFilter filter : filters) {serializer.addFilter(filter);}}serializer.write(object);return out.toString();} finally {out.close();}
}
往下到 serializer.write 方法:
public final void write(Object object) {if (object == null) {out.writeNull();return;}Class<?> clazz = object.getClass();ObjectSerializer writer = getObjectWriter(clazz);try {writer.write(this, object, null, null, 0);} catch (IOException e) {throw new JSONException(e.getMessage(), e);}}
再到 getObjectWriter,注意入參create傳了true:
public ObjectSerializer getObjectWriter(Class<?> clazz) {return getObjectWriter(clazz, true);
}
在 getObjectWriter 的核心具體實現中,走到了自定義對象序列化的流程:
// ......
if (create) {writer = createJavaBeanSerializer(clazz);put(clazz, writer);
}
createJavaBeanSerializer 往下到 TypeUtils.buildBeanInfo:
public final ObjectSerializer createJavaBeanSerializer(Class<?> clazz) {SerializeBeanInfo beanInfo = TypeUtils.buildBeanInfo(clazz, null, propertyNamingStrategy, fieldBased);if (beanInfo.fields.length == 0 && Iterable.class.isAssignableFrom(clazz)) {return MiscCodec.instance;}return createJavaBeanSerializer(beanInfo);
}
在 buildBeanInfo 中,由于入參 fieldBased 是false,會走到 computeGetters 的邏輯:
List<FieldInfo> fieldInfoList = fieldBased? computeGettersWithFieldBase(beanType, aliasMap, false, propertyNamingStrategy) //: computeGetters(beanType, jsonType, aliasMap, fieldCacheMap, false, propertyNamingStrategy);
看到 computeGetters 的名字,感覺八成是這里了,發現里面有一段邏輯是掃描以 get 開頭的方法名,把方法后綴變成一個屬性,后續在獲取對應屬性時,會去運行對應的 getter 方法:
if(methodName.startsWith("get")){// 省略...// 從方法名中解析出屬性名propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
從上面這段代碼可以獲取到 propertyName 的值為 idToLong,并且對應的 fieldInfo 是 getIdToLong 方法。
到這里基本水落石出了,原來是fastjson序列化是掃描以 “get”(還有“is”) 開頭的方法,并且從該方法名中提取屬性,如果對應的方法中存在問題,那么這里就可能遇到對應的異常,就像本文遇到的NPE。
解決方案
1、 業務邏輯中處理:保證 node 對象中的 orgId 不為空,避免NPE。
2、日志打印中處理:不序列化整個對象,只打出關鍵信息,避開可能為空的字段。
3、 在調用JSON.toJSONString的時候,加上SerializerFeature.IgnoreNonFieldGetter參數,忽略掉所有沒有對應成員變量(Field)的getter函數,可以正常序列化。
JSONObject.toJSONString(node, SerializerFeature.IgnoreNonFieldGetter)
4、 通過在函數上 getXxx() 增加@JSONField(serialize = false)注解,也能達到同樣的效果。
@JSONField(serialize = false)
public Long getIdToLong() {return Long.valueOf(this.id);
}
computeGetters 中消費注解的代碼:
JSONField annotation = method.getAnnotation(JSONField.class);// ...if(annotation != null){if(!annotation.serialize()){continue;}// ...if(methodName.startsWith("get")){
// ...
總結
fastjson 將對象轉為 string 時,會把以“get”開頭的方法認為是屬性的 getter,把 getXXX 方法后面的 XXX 變成一個屬性,并通過 getXXX 方法去獲取,如果get方法內存在異常邏輯,就可能報錯。可以盡量避免使用JSON打日志。
附錄
1、阿里巴巴開發規約
2、默認根據get方法進行序列化,根據java bean的定義,通過反射來獲取,javaBean定義見:什么是JavaBean、bean?