Quickly replace required content with {} in JAVA strings

The method of printing logs based on slf4j is easier to use and can be extracted and used elsewhere.

Background

Whether it is a single application or a massive application, it starts from a small application and slowly evolves. As the application gradually grows larger and the business logic becomes more complex, the system begins to become unstable. At this time, you need to split the system or reconstruct it. But the cost of renovation is relatively high. At this time, you need to consider manpower, material resources, time, etc. If the system is not beyond hope (it can still run), it is best to simply check the error and print the log. Printing logs is accompanied by the efficiency of string splicing. Printing one line of code System.out.print() is easy for the system, but replacing hundreds of thousands to millions or even tens of millions of characters requires efficiency. Higher alternative.

Introduction

Anyone who has actually written an enterprise-level application knows that printing logs is to facilitate us in troubleshooting problems. From simple log to various levels of output, to massive logs to trace the call chain through requestId or traceId road.
The splicing method of these logs is achieved through string interception, splicing, and recombination.
In the Java language, commonly used logging systems include log4j, logback, jul, etc. Among them, in order to achieve the fast replacement of log system implementation and application, slf4j implemented in facade mode and bridge mode all need to face fast string processing.
This article mainly briefly describes the replacement process of strings with {} in combination with slf4j and log4j2.

Print

The demonstration here is based on the Maven project. First, we need to write a simple string of code and write the log4j.properties file into resource.
dependencies of the pom.xml file:

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

Simple printing:

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class Slf4jTest {
    public static void main(String[] args) {
// The slf4j annotation of the lombok plug-in is used here.
// The log source code is in org.slf4j.Logger
//The actual call is org.slf4j.impl.Log4jLoggerAdapter, which implements a wrapper to wrap the implementation of log4j org.apache.log4j.Logger
        log.info("qqq{}ppp", 123);
        //Print results:
[INFO] 2020-06-10 22:56:50,872 method:com.test.slf4j.Slf4jTest.main(Slf4jTest.java:9)qqq123ppp
    }
}

Configuration of log4j.properties

log4j.rootLogger=info,stdout # info level log, the print appender (appender) of stdout identifier takes effect

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

If we debug the code, we will find that when calling the slf4j code, the program has already spliced the string, and its calling link is

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)

The most critical code here is MessageFormatter, which implements a set of algorithms based on StringBuilder and string dynamic indexing. The code is as follows:

int i = 0;
int j;
 // use string builder for better multicore performance
 // StringBuilder to improve multi-core performance, lock-free, non-thread-safe, and advance 50 character bits
 StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);

 int L;
 // argArray is the Object array for input replacement, here is new Object[]{ 123 }
 for (L = 0; L < argArray.length; L + + ) {

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

\t// none {}
     if (j == -1) {
         // no more variables
         if (i == 0) { // this is a simple string
         //return directly
             return new FormattingTuple(messagePattern, argArray, throwable);
         } else { // add the tail string which contains no variables and return
             // the result.
             // Also need to truncate characters
             sbuf.append(messagePattern, i, messagePattern.length());
             return new FormattingTuple(sbuf.toString(), argArray, throwable);
         }
     } else {
     // Not an escape character, like this: \{}
         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);
             // It comes with array writing, such as boolean[], char[], etc. Even if the passed in argArray[L] is a basic type, the object type can be converted through automatic assembly.
             deeplyAppendParameter(sbuf, argArray[L], new HashMap<Object[], Object>());
             i = j + 2;
         }
     }
 }
 // append the characters following the last {} pair.
 sbuf.append(messagePattern, i, messagePattern.length());

Extension

If you want to replace and split a large number of characters in your project, you can actually reuse the MessageFormatter class. After countless tests, the stability and efficiency must be very high.
So based on MessageFormatter, a simplified version of MessageFormatter was rewritten and only returned a string:

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 & amp; & amp; 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(']');
    }

}

Test Results:

System.out.println(MessageFormatter.format("qqq{}ppp{}end", 123, 321));
Print result: qqq123ppp321end