slf4j 中的字符串 {} 快速替換

背景

不管是單體應用還是海量應用,都是從小應用開始慢慢演進。當應用逐漸變大,業務邏輯變得更加複雜之後,系統開始變得不穩定,這時候你需要將系統拆分,或者重構。但改造的成本是相對高昂的,這時候你需要考慮的是人力、物力、時間等等。如果系統還沒有到無可救藥的地步(還能跑),簡單的查錯打印日誌是再好不過的了。打印日誌伴隨着字符串拼接的效率問題,一行代碼打印System.out.print()對系統來說輕而易舉,但十萬到百萬甚至千萬的字符替換就需要考慮效率更高的替換方式。

簡介

如果實際寫過企業級應用的人都知道,打印日誌都是爲了方便我們排查問題。從簡單的log各種級別的output,到海量日誌中通過requestIdtraceId去跟蹤調用鏈路。
這些日誌的拼接方式,都要通過字符串截取、拼接、再組合來實現。
Java語言中,常用的日誌系統有log4jlogbackjul等。其中爲了達到日誌系統實現與應用快速替換的方式,採用門面模式和橋接模式實現的slf4j,都需要面對快速的字符串處理。
該篇文章主要簡述slf4jlog4j2結合在處理帶{}字符串的替換過程。

打印

這裏是基於Maven項目來演示,首先我們需要寫一串簡單的代碼,並將log4j.properties文件寫入resource中。
pom.xml文件的dependencies

 <dependency>
     <groupId>org.slf4j</groupId>
     <artifactId>slf4j-log4j12</artifactId>
     <version>1.7.30</version>
 </dependency>

簡單打印:

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class Slf4jTest {
    public static void main(String[] args) {
	    // 這裏使用 lombok 插件的 slf4j 註解
	    // log 源碼在 org.slf4j.Logger
	    // 實際調用爲 org.slf4j.impl.Log4jLoggerAdapter ,它實現了一個 wrapper,去包裝實現 log4j org.apache.log4j.Logger
        log.info("qqq{}ppp", 123); 
        // 打印結果:
[INFO ] 2020-06-10 22:56:50,872 method:com.test.slf4j.Slf4jTest.main(Slf4jTest.java:9)qqq123ppp
    }
}

log4j.properties的配置

log4j.rootLogger=info,stdout # info 級別的日誌,stdout 標誌符的打印追加器(appender)生效

log4j.appender.stdout= org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target= System.out
log4j.appender.stdout.layout= org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern= [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%m%n

我們如果debug代碼的話,會發現在調用slf4j代碼的時候,程序已經拼接好了字符串,它的調用鏈路是

org.slf4j.impl.Log4jLoggerAdapter#info(java.lang.String, java.lang.Object) 
	-> org.slf4j.helpers.MessageFormatter#format(java.lang.String, java.lang.Object)
	-> org.slf4j.helpers.MessageFormatter#arrayFormat(java.lang.String, java.lang.Object[])
	-> org.slf4j.helpers.MessageFormatter#arrayFormat(java.lang.String, java.lang.Object[], java.lang.Throwable)

在這裏最關鍵的代碼是MessageFormatter,它實現了一套基於StringBuilder和字符串動態索引的算法,代碼如下:

int i = 0;
int j;
 // use string builder for better multicore performance
 // StringBuilder 來提高多核性能,無鎖,非線程安全,並提前預支了 50 個字符位
 StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);

 int L;
 // argArray 是輸入替換的 Object 數組,這裏是 new Object[]{ 123 }
 for (L = 0; L < argArray.length; L++) {

	// DELIM_STR == {} 字符
     j = messagePattern.indexOf(DELIM_STR, i);

	// 無 {}
     if (j == -1) {
         // no more variables
         if (i == 0) { // this is a simple string
         	// 直接返回
             return new FormattingTuple(messagePattern, argArray, throwable);
         } else { // add the tail string which contains no variables and return
             // the result.
             // 還需要截斷下字符
             sbuf.append(messagePattern, i, messagePattern.length());
             return new FormattingTuple(sbuf.toString(), argArray, throwable);
         }
     } else {
     	// 不是轉義字符,類似這種:\{}
         if (isEscapedDelimeter(messagePattern, j)) {
             if (!isDoubleEscaped(messagePattern, j)) {
                 L--; // DELIM_START was escaped, thus should not be incremented
                 sbuf.append(messagePattern, i, j - 1);
                 sbuf.append(DELIM_START);
                 i = j + 1;
             } else {
                 // The escape character preceding the delimiter start is
                 // itself escaped: "abc x:\\{}"
                 // we have to consume one backward slash
                 sbuf.append(messagePattern, i, j - 1);
                 deeplyAppendParameter(sbuf, argArray[L], new HashMap<Object[], Object>());
                 i = j + 2;
             }
         } else {
             // normal case
             sbuf.append(messagePattern, i, j);
             // 其中附帶數組寫入,如 boolean[],char[]等,argArray[L] 傳入的即使是基本類型,也可以通過自動裝配,實現對象類型的轉換
             deeplyAppendParameter(sbuf, argArray[L], new HashMap<Object[], Object>());
             i = j + 2;
         }
     }
 }
 // append the characters following the last {} pair.
 sbuf.append(messagePattern, i, messagePattern.length());

擴展

如果要在項目中對大量字符進行替換拆分,其實可以複用MessageFormatter這個類。經過了無數次考驗,穩定性和效率那必定是很高的了。
所以基於MessageFormatter重新複寫了一個簡版的MessageFormatter只返回字符串:


public class MessageFormatter {

    private static final char DELIM_START = '{';
    private static final String DELIM_STR = "{}";
    private static final char ESCAPE_CHAR = '\\';

    public static String format(final String messagePattern, final Object... argArray) {

        int i = 0;
        int j;
        // use string builder for better multicore performance
        StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);

        int L;
        for (L = 0; L < argArray.length; L++) {

            j = messagePattern.indexOf(DELIM_STR, i);

            if (j == -1) {
                // no more variables
                if (i == 0) { // this is a simple string
                    return messagePattern;
                } else { // add the tail string which contains no variables and return
                    // the result.
                    sbuf.append(messagePattern, i, messagePattern.length());
                    return sbuf.toString();
                }
            } else {
                if (isEscapedDelimeter(messagePattern, j)) {
                    if (!isDoubleEscaped(messagePattern, j)) {
                        L--; // DELIM_START was escaped, thus should not be incremented
                        sbuf.append(messagePattern, i, j - 1);
                        sbuf.append(DELIM_START);
                        i = j + 1;
                    } else {
                        // The escape character preceding the delimiter start is
                        // itself escaped: "abc x:\\{}"
                        // we have to consume one backward slash
                        sbuf.append(messagePattern, i, j - 1);
                        deeplyAppendParameter(sbuf, argArray[L], new HashMap<>());
                        i = j + 2;
                    }
                } else {
                    // normal case
                    sbuf.append(messagePattern, i, j);
                    deeplyAppendParameter(sbuf, argArray[L], new HashMap<>());
                    i = j + 2;
                }
            }
        }
        // append the characters following the last {} pair.
        sbuf.append(messagePattern, i, messagePattern.length());
        return sbuf.toString();
    }

    private static boolean isDoubleEscaped(String messagePattern, int delimeterStartIndex) {
        return delimeterStartIndex >= 2 && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR;
    }

    private static boolean isEscapedDelimeter(String messagePattern, int delimeterStartIndex) {
        if (delimeterStartIndex == 0) {
            return false;
        }
        char potentialEscape = messagePattern.charAt(delimeterStartIndex - 1);
        return potentialEscape == ESCAPE_CHAR;
    }

    // special treatment of array values was suggested by 'lizongbo'
    private static void deeplyAppendParameter(StringBuilder sbuf, Object o, Map<Object[], Object> seenMap) {
        if (o == null) {
            sbuf.append("null");
            return;
        }
        if (!o.getClass().isArray()) {
            safeObjectAppend(sbuf, o);
        } else {
            // check for primitive array types because they
            // unfortunately cannot be cast to Object[]
            if (o instanceof boolean[]) {
                booleanArrayAppend(sbuf, (boolean[]) o);
            } else if (o instanceof byte[]) {
                byteArrayAppend(sbuf, (byte[]) o);
            } else if (o instanceof char[]) {
                charArrayAppend(sbuf, (char[]) o);
            } else if (o instanceof short[]) {
                shortArrayAppend(sbuf, (short[]) o);
            } else if (o instanceof int[]) {
                intArrayAppend(sbuf, (int[]) o);
            } else if (o instanceof long[]) {
                longArrayAppend(sbuf, (long[]) o);
            } else if (o instanceof float[]) {
                floatArrayAppend(sbuf, (float[]) o);
            } else if (o instanceof double[]) {
                doubleArrayAppend(sbuf, (double[]) o);
            } else {
                objectArrayAppend(sbuf, (Object[]) o, seenMap);
            }
        }
    }


    private static void safeObjectAppend(StringBuilder sbuf, Object o) {
        try {
            String oAsString = o.toString();
            sbuf.append(oAsString);
        } catch (Throwable t) {
            Util.report("SLF4J: Failed toString() invocation on an object of type [" + o.getClass().getName() + "]", t);
            sbuf.append("[FAILED toString()]");
        }

    }

    private static void objectArrayAppend(StringBuilder sbuf, Object[] a, Map<Object[], Object> seenMap) {
        sbuf.append('[');
        if (!seenMap.containsKey(a)) {
            seenMap.put(a, null);
            final int len = a.length;
            for (int i = 0; i < len; i++) {
                deeplyAppendParameter(sbuf, a[i], seenMap);
                if (i != len - 1)
                    sbuf.append(", ");
            }
            // allow repeats in siblings
            seenMap.remove(a);
        } else {
            sbuf.append("...");
        }
        sbuf.append(']');
    }


    private static void booleanArrayAppend(StringBuilder sbuf, boolean[] a) {
        sbuf.append('[');
        final int len = a.length;
        for (int i = 0; i < len; i++) {
            sbuf.append(a[i]);
            if (i != len - 1)
                sbuf.append(", ");
        }
        sbuf.append(']');
    }

    private static void byteArrayAppend(StringBuilder sbuf, byte[] a) {
        sbuf.append('[');
        final int len = a.length;
        for (int i = 0; i < len; i++) {
            sbuf.append(a[i]);
            if (i != len - 1)
                sbuf.append(", ");
        }
        sbuf.append(']');
    }

    private static void charArrayAppend(StringBuilder sbuf, char[] a) {
        sbuf.append('[');
        final int len = a.length;
        for (int i = 0; i < len; i++) {
            sbuf.append(a[i]);
            if (i != len - 1)
                sbuf.append(", ");
        }
        sbuf.append(']');
    }

    private static void shortArrayAppend(StringBuilder sbuf, short[] a) {
        sbuf.append('[');
        final int len = a.length;
        for (int i = 0; i < len; i++) {
            sbuf.append(a[i]);
            if (i != len - 1)
                sbuf.append(", ");
        }
        sbuf.append(']');
    }

    private static void intArrayAppend(StringBuilder sbuf, int[] a) {
        sbuf.append('[');
        final int len = a.length;
        for (int i = 0; i < len; i++) {
            sbuf.append(a[i]);
            if (i != len - 1)
                sbuf.append(", ");
        }
        sbuf.append(']');
    }

    private static void longArrayAppend(StringBuilder sbuf, long[] a) {
        sbuf.append('[');
        final int len = a.length;
        for (int i = 0; i < len; i++) {
            sbuf.append(a[i]);
            if (i != len - 1)
                sbuf.append(", ");
        }
        sbuf.append(']');
    }

    private static void floatArrayAppend(StringBuilder sbuf, float[] a) {
        sbuf.append('[');
        final int len = a.length;
        for (int i = 0; i < len; i++) {
            sbuf.append(a[i]);
            if (i != len - 1)
                sbuf.append(", ");
        }
        sbuf.append(']');
    }

    private static void doubleArrayAppend(StringBuilder sbuf, double[] a) {
        sbuf.append('[');
        final int len = a.length;
        for (int i = 0; i < len; i++) {
            sbuf.append(a[i]);
            if (i != len - 1)
                sbuf.append(", ");
        }
        sbuf.append(']');
    }

}

測試結果:

System.out.println(MessageFormatter.format("qqq{}ppp{}end", 123, 321));
打印結果:qqq123ppp321end
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章