
前言
周三的時(shí)候,組內(nèi)出現(xiàn)了一個(gè)線上問(wèn)題,影響到了若干個(gè)用戶的下單、支付等操作。然而實(shí)際查詢到問(wèn)題的原因時(shí),發(fā)現(xiàn)只是由于一行小小的日志打印導(dǎo)致的錯(cuò)誤。
以下的文章內(nèi)容分為主要分為三部分:
1、對(duì)案件的發(fā)生進(jìn)行回顧;
2、分析案件發(fā)生的原因;
3、對(duì)案件總結(jié)與反思
以三章內(nèi)容來(lái)回顧出現(xiàn)的問(wèn)題,以及提供未來(lái)的預(yù)防策略。
案件回顧
周三的時(shí)候,服務(wù)頻繁收到報(bào)警,系統(tǒng)頻繁爆出空指針異常。值班同學(xué)根據(jù)報(bào)錯(cuò)的錯(cuò)誤棧,快速定位到了錯(cuò)誤的代碼行。
at com.alibaba.fastjson.serializer.JSONSerializer.write(JSONSerializer.java:285)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:696)
找到代碼行后卻讓值班同學(xué)感到疑惑:“這個(gè)明顯是fastjson的日志打印呀,這也會(huì)有什么錯(cuò)誤么?”。旁邊的同事看完卻驚呼一聲:“fastJson打印日志會(huì)調(diào)用對(duì)象內(nèi)的其余的get方法的呀!”。
(PS:該對(duì)象是一個(gè)DDD的核心域?qū)ο螅渲邪恍I(yè)務(wù)場(chǎng)景方法被命名為getXXX方法的,因此執(zhí)行Json序列化打印也就可能因?yàn)椴糠謹(jǐn)?shù)據(jù)為空而出現(xiàn)空指針。)
定位到了問(wèn)題原因,本著優(yōu)先止損的原則,值班同事快速上線代碼刪除了這行日志打印。系統(tǒng)暫時(shí)的恢復(fù)了正常,沒(méi)有再出現(xiàn)新增的報(bào)錯(cuò)信息了。然而后續(xù)還有漫長(zhǎng)的數(shù)據(jù)修復(fù)、更正的過(guò)程。
案件分析:
案件復(fù)原:
本質(zhì)上來(lái)說(shuō),這起線上事故出現(xiàn)的原因主要是因?yàn)閒astJson序列化時(shí),會(huì)將手工編寫(xiě)的一些方法認(rèn)為是待輸出屬性對(duì)象,那么如果這些方法包含一些業(yè)務(wù)邏輯代碼的時(shí)候,就會(huì)存在出現(xiàn)異常的風(fēng)險(xiǎn)。這里我們簡(jiǎn)單復(fù)現(xiàn)一下場(chǎng)景:
@Data
public class CoreData {
//正常的屬性
public String normalProperties = "normalProperties";
/**
* 以get開(kāi)頭的方法 不是期望輸出的屬性
* @return
*/
public String getFuncProperties(){
return "getFuncProperties";
}
/**
* 以is開(kāi)頭的方法 不是期望輸出的屬性
* @return
*/
public Boolean isType(){
return true;
}
}
如上代碼是我們編寫(xiě)的一個(gè)純代碼類(lèi),可以看到,我們實(shí)際期望設(shè)置的屬性應(yīng)該只有一個(gè)normalPropertites。
public static void main(String[] args) {
CoreData data = new CoreData();
String dataString = JSONObject.toJSONString(data);
System.out.println(dataString); // 對(duì)應(yīng)正常的業(yè)務(wù)邏輯
}
進(jìn)而我還寫(xiě)了一段針對(duì)當(dāng)前對(duì)象進(jìn)行打印的代碼,從上可以看到,就是簡(jiǎn)單的對(duì)對(duì)象進(jìn)行JSON序列化后打印輸出。按照我們的期望來(lái)說(shuō),只是期望輸出normalProperties這一個(gè)固有的字符串屬性。隨后我運(yùn)行了代碼,得到了如下的結(jié)果:

可以看到,一個(gè)類(lèi)型+兩個(gè)方法,都被JSON序列化后輸出了。那么如果此時(shí)我們?cè)趃etFuncProperties()這樣的方法中如果出現(xiàn)了異常,就會(huì)影響整個(gè)業(yè)務(wù)的運(yùn)行。例如我們把方法改成如下的例子:
public String getFuncProperties(){
double a = 2/0;
return "getFuncProperties";
}

可以看到,我們?cè)镜倪壿嬁赡苤皇窍胼敵鰊ormalProperties屬性,但是因?yàn)間etFuncProperties2/0是無(wú)法進(jìn)行運(yùn)算的,導(dǎo)致了系統(tǒng)直接報(bào)錯(cuò)了。那么此時(shí),main函數(shù)中的輸出方法(對(duì)應(yīng)于我們正常業(yè)務(wù)邏輯),也就無(wú)法再繼續(xù)執(zhí)行了,而這在生產(chǎn)環(huán)境上無(wú)疑是致命的。
背后原理:
(PS: 以下討論內(nèi)容均基于1.2.9版本的fastJson。)
根據(jù)報(bào)錯(cuò)的問(wèn)題點(diǎn),結(jié)合debug,很快找到了問(wèn)題所在:

在**com.alibaba.fastjson.serializer.JSONSerializer#write(java.lang.Object)**這個(gè)方法中,F(xiàn)astjson所創(chuàng)建的ObjectSerializer對(duì)象中,nature下所包含的getters對(duì)象有三個(gè)。這明顯不符合我們的預(yù)期。那么我們就需要找到他是如何獲取到這三個(gè)方法的。緊跟著我們進(jìn)行追入,在`com.alibaba.fastjson.serializer.SerializeConfig#getObjectWriter`方法下找到了這行代碼:
put(clazz, createJavaBeanSerializer(clazz));
很明顯,這里的createJavaBeanSerializer(clazz)創(chuàng)建了javaBean的序列化器。對(duì)于該方法,其主要的邏輯流程就是判斷當(dāng)前的對(duì)象類(lèi)型是否符合使用ASM的序列化器。這里一通判斷下來(lái),是符合采用ASM序列化的要求的,因此,我們又進(jìn)一步定位到了如下代碼:
ObjectSerializer asmSerializer = createASMSerializer(clazz);
在createASMSerializer對(duì)應(yīng)的方法中,最關(guān)鍵的代碼莫過(guò)于下面這行了:
List<FieldInfo> unsortedGetters = TypeUtils.computeGetters(clazz, jsonType, aliasMap, false);
這力的代碼會(huì)生成對(duì)應(yīng)的fieldInfo對(duì)象,也正好對(duì)應(yīng)了前面我們涉及到的那三個(gè)方法,這里讓我們仔細(xì)看一下com.alibaba.fastjson.util.TypeUtils#computeGetters所對(duì)應(yīng)的代碼:
public static List<FieldInfo> computeGetters(Class<?> clazz, JSONType jsonType, Map<String, String> aliasMap, boolean sorted) {
Map<String, FieldInfo> fieldInfoMap = new LinkedHashMap<String, FieldInfo>();
for (Method method : clazz.getMethods()) {
String methodName = method.getName();
int ordinal = 0, serialzeFeatures = 0;
String label = null;
//判讀當(dāng)前方法是否為靜態(tài)的
if (Modifier.isStatic(method.getModifiers())) {
continue;
}
//若返回值為void則此時(shí)不需要處理
if (method.getReturnType().equals(Void.TYPE)) {
continue;
}
//若此時(shí)入?yún)⒉粸榭談t跳過(guò)
if (method.getParameterTypes().length != 0) {
continue;
}
//若返回類(lèi)型是類(lèi)加載器也進(jìn)行跳過(guò)。
if (method.getReturnType() == ClassLoader.class) {
continue;
}
//若方法名是getMetaClass也跳過(guò)
if (method.getName().equals("getMetaClass")
&& method.getReturnType().getName().equals("groovy.lang.MetaClass")) {
continue;
}
//獲取方法的有關(guān)JSONField的注釋
JSONField annotation = method.getAnnotation(JSONField.class);
if (annotation == null) {
//若當(dāng)前類(lèi)為空,則再獲取父類(lèi)的。
annotation = getSupperMethodAnnotation(clazz, method);
}
if (annotation != null) {
//若父類(lèi)不為空則進(jìn)行序列化的判斷,我們使用的例子無(wú)繼承,這部分先忽略不看。
......
}
//重點(diǎn)來(lái)了,判斷當(dāng)前是否以get開(kāi)頭
if (methodName.startsWith("get")) {
//長(zhǎng)度小于4,即不滿足getXX的格式的,直接跳過(guò)。
if (methodName.length() < 4) {
continue;
}
//getClass的進(jìn)行跳過(guò)
if (methodName.equals("getClass")) {
continue;
}
//獲取第四個(gè)位置的字符
char c3 = methodName.charAt(3);
String propertyName;
if (Character.isUpperCase(c3) || c3 > 512 ) {
//若方法遵循駝峰的寫(xiě)法:則依次取出對(duì)應(yīng)的名稱信息
if (compatibleWithJavaBean) {
propertyName = decapitalize(methodName.substring(3));
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
} else if (...) {
//這里針對(duì)部分特殊的寫(xiě)法:如get_X、getfX做了特殊的判斷處理。
} else {
continue;
}
// 方案一 - 這里會(huì)根據(jù)當(dāng)前屬性名和clazz來(lái)判斷是否被忽略了,詳見(jiàn)@JsonType注解
boolean ignore = isJSONTypeIgnore(clazz, propertyName);
// 如果忽略了,就不再往下走了
if (ignore) {
continue;
}
//此時(shí)根據(jù)屬性和類(lèi)獲取對(duì)應(yīng)的值對(duì)象。
Field field = ParserConfig.getField(clazz, propertyName);
JSONField fieldAnnotation = null;
if (field != null) {
//方案二 - 會(huì)獲取屬性對(duì)應(yīng)的JSONField注解
// 如果該注解的serialize屬性是false,那么也不會(huì)繼續(xù)往下去加載邏輯
fieldAnnotation = field.getAnnotation(JSONField.class);
if (fieldAnnotation != null) {
if (!fieldAnnotation.serialize()) {
continue;
}
//獲取順序
ordinal = fieldAnnotation.ordinal();
serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
if (fieldAnnotation.name().length() != 0) {
//獲取名字
propertyName = fieldAnnotation.name();
if (aliasMap != null) {
propertyName = aliasMap.get(propertyName);
if (propertyName == null) {
continue;
}
}
}
if (fieldAnnotation.label().length() != 0) {
label = fieldAnnotation.label();
}
}
}
if (aliasMap != null) {
propertyName = aliasMap.get(propertyName);
if (propertyName == null) {
continue;
}
}
//這里會(huì)新構(gòu)建一個(gè)fieldInfo對(duì)象,并存放到fieldInfoMap中進(jìn)行保存
FieldInfo fieldInfo = new FieldInfo(propertyName, method, field, clazz, null, ordinal, serialzeFeatures,
annotation, fieldAnnotation, label);
fieldInfoMap.put(propertyName, fieldInfo);
}
//緊接著第二部分是關(guān)于isXXX的方法
if (methodName.startsWith("is")) {
if (methodName.length() < 3) {
continue;
}
char c2 = methodName.charAt(2);
String propertyName;
if (Character.isUpperCase(c2)) {
if (compatibleWithJavaBean) {
propertyName = decapitalize(methodName.substring(2));
} else {
propertyName = Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3);
}
} else if (...) {
//同上面幾乎一樣,也是針對(duì)is_x這類(lèi)特殊寫(xiě)法做了處理。
}else {
continue;
}
Field field = ParserConfig.getField(clazz, propertyName);
if (field == null) {
field = ParserConfig.getField(clazz, methodName);
}
JSONField fieldAnnotation = null;
if (field != null) {
//同樣是對(duì)JSONField注解做處理。
fieldAnnotation = field.getAnnotation(JSONField.class);
if (fieldAnnotation != null) {
if (!fieldAnnotation.serialize()) {
continue;
}
ordinal = fieldAnnotation.ordinal();
serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
if (fieldAnnotation.name().length() != 0) {
propertyName = fieldAnnotation.name();
if (aliasMap != null) {
propertyName = aliasMap.get(propertyName);
if (propertyName == null) {
continue;
}
}
}
if (fieldAnnotation.label().length() != 0) {
label = fieldAnnotation.label();
}
}
}
if (aliasMap != null) {
propertyName = aliasMap.get(propertyName);
if (propertyName == null) {
continue;
}
}
FieldInfo fieldInfo = new FieldInfo(propertyName, method, field, clazz, null, ordinal, serialzeFeatures,
annotation, fieldAnnotation, label);
fieldInfoMap.put(propertyName, fieldInfo);
}
}
//最后,又是對(duì)所有的常規(guī)屬性做相應(yīng)的處理,避免因?yàn)槟硞€(gè)屬性沒(méi)寫(xiě)getX()方法而得不到序列化。整體的加載邏輯同上。
for (Field field : clazz.getFields()) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
JSONField fieldAnnotation = field.getAnnotation(JSONField.class);
int ordinal = 0, serialzeFeatures = 0;
String propertyName = field.getName();
String label = null;
if (fieldAnnotation != null) {
if (!fieldAnnotation.serialize()) {
continue;
}
ordinal = fieldAnnotation.ordinal();
serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
if (fieldAnnotation.name().length() != 0) {
propertyName = fieldAnnotation.name();
}
if (fieldAnnotation.label().length() != 0) {
label = fieldAnnotation.label();
}
}
if (aliasMap != null) {
propertyName = aliasMap.get(propertyName);
if (propertyName == null) {
continue;
}
}
if (!fieldInfoMap.containsKey(propertyName)) {
FieldInfo fieldInfo = new FieldInfo(propertyName, null, field, clazz, null, ordinal, serialzeFeatures,
null, fieldAnnotation, label);
fieldInfoMap.put(propertyName, fieldInfo);
}
}
List<FieldInfo> fieldInfoList = new ArrayList<FieldInfo>();
boolean containsAll = false;
String[] orders = null;
JSONType annotation = clazz.getAnnotation(JSONType.class);
if (annotation != null) {
orders = annotation.orders();
if (orders != null && orders.length == fieldInfoMap.size()) {
containsAll = true;
for (String item : orders) {
if (!fieldInfoMap.containsKey(item)) {
containsAll = false;
break;
}
}
} else {
containsAll = false;
}
}
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);
}
}
return fieldInfoList;
}
代碼有點(diǎn)長(zhǎng),聽(tīng)我一點(diǎn)點(diǎn)地慢慢解釋。整個(gè)代碼其實(shí)比較容易理解,我嘗試從我們常規(guī)角度來(lái)理解下。fastJson組件的發(fā)明者認(rèn)為,類(lèi)中常見(jiàn)需要序列化的類(lèi)型有三種:
1、getX()方法;
2、isX()方法;
3、沒(méi)有寫(xiě)getX()方法的固有變量。
圍繞這三種類(lèi)型他做的事都是類(lèi)似的。這里我們先以getX()方法為例子展開(kāi)說(shuō)明,要獲取到所有的getX()方法,并對(duì)他們解析,主要分為以下四個(gè)步驟:
1、獲取到所有的類(lèi)下的方法信息
這個(gè)可以通過(guò)class<?>.getMethods()方法獲得,如下是我coreData類(lèi)的所有方法。

2、判斷符合規(guī)范的getXXX方法
在獲取到了所有的method以后,我們自然需要判斷哪些是符合規(guī)范的getXX方法。在組件中是這么判斷的:
if (methodName.startsWith("get")) {
//此時(shí)做相應(yīng)的處理邏輯
}
沒(méi)錯(cuò),就是這么粗暴簡(jiǎn)單。
3、根據(jù)JSONType判斷是否需要加載
那么獲取到這些方法就一定要加載了嗎?當(dāng)然不是!對(duì)于getter方法,fastJson會(huì)首先判斷當(dāng)前的屬性,是否已被包含在了類(lèi)的`@JSONType(ignores = "xxx")`下,如果包含在了其中,那么此時(shí)就不會(huì)去將該方法保存到待序列化的列表中。局限點(diǎn)在于該種寫(xiě)法只會(huì)對(duì)get方法生效,對(duì)于isXXX和普通屬性是不會(huì)生效的。
// 方案一 - 這里會(huì)根據(jù)當(dāng)前屬性名和clazz來(lái)判斷是否被忽略了,詳見(jiàn)@JsonType注解
boolean ignore = isJSONTypeIgnore(clazz, propertyName);
// 如果忽略了,就不再往下走了
if (ignore) {
continue;
}

4、根據(jù)JSONField判斷是否需要加載
什么?你說(shuō)采用JSONType寫(xiě)一大堆不方便?fastJson自然也是想到了,那么此時(shí)就可以采用`@JSONField(serialize = false)`的方式在對(duì)單獨(dú)的屬性或方法進(jìn)行標(biāo)注。也能起到忽略的作用。

到此,以getXX()方法的解析判斷就完成了,當(dāng)然其中還有一些更為細(xì)致的判斷邏輯,如跳過(guò)getMetaClass、返回值為空的跳過(guò)等等邏輯。但大體上已經(jīng)不影響我們的分析了。isXXX和固有變亮的解析幾乎相似。至此,我們已經(jīng)大致了解了整個(gè)解析的原理。當(dāng)然為了驗(yàn)證我們的邏輯的正確性,我對(duì)原本coreData的代碼做了一下改造并進(jìn)行了試驗(yàn),具體內(nèi)容如下所示:
@Data
@JSONType(ignores = "funcProperties")
public class CoreData {
//正常的屬性
public String normalProperties = "normalProperties";
/**
* 以get開(kāi)頭的方法
* @return
*/
public String getFuncProperties(){
double a = 2/0;
return "getFuncProperties";
}
/**
* 以is開(kāi)頭的方法
* @return
*/
@JSONField(serialize = false)
public Boolean isType(){
return true;
}
/**
* 用于跳過(guò),檢查方法是否判斷
* @return
*/
public String skipFuncProperties(){
double a = 2/0;
return "getFuncProperties";
}
}
簡(jiǎn)要來(lái)說(shuō),這里對(duì)getFuncProperties方法,我才用了`@JSONType(ignores = "funcProperties")`將其進(jìn)行忽略,而對(duì)于isType方法,我則用單個(gè)的`@JSONField(serialize = false)`對(duì)其進(jìn)行忽略,如果我們的結(jié)論成立,那么此時(shí)應(yīng)該只會(huì)保存一個(gè)normalProperties屬性的輸出,且不存在出現(xiàn)報(bào)錯(cuò)的情況。

事實(shí)證明,我們是對(duì)的。
案件總結(jié)與反思:
在經(jīng)歷了這次慘痛的教訓(xùn)之后,有哪些是值得我們深入關(guān)注去思考和反思的呢?
1、在編寫(xiě)方法的時(shí)候盡量避免才用getXXX、isXXX的方法進(jìn)行書(shū)寫(xiě),這會(huì)導(dǎo)致部分框架的解析出現(xiàn)問(wèn)題。(這個(gè)點(diǎn)也是我曾經(jīng)在JAVA開(kāi)發(fā)手冊(cè)中看到的,想必也是前人被坑過(guò)了。)
2、如果非要這樣寫(xiě),那么此時(shí)需要評(píng)估好當(dāng)前這個(gè)方法是否需要被一些框架進(jìn)行解析,如果不需要,嘗試對(duì)這些類(lèi)型屬性添加基本的忽略操作。類(lèi)似@JSONField(seralize = false)、@Trasient等注解。
3、避免在對(duì)象中參雜進(jìn)復(fù)雜的業(yè)務(wù)邏輯。(當(dāng)然這條并不一定正常,對(duì)于DDD的充血模型,有時(shí)候是需要一定的業(yè)務(wù)邏輯的混合的。)
吃一塹長(zhǎng)一智,如此一來(lái)才能避免在未來(lái)犯下相同的錯(cuò)誤呀~