關(guān)于我加了一行日志搞崩了服務(wù)這件小事

前言

周三的時(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ò)誤呀~
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容