名字由来

The Simple Logging Facade for Java (SLF4J) serves as a simple facade or abstraction for various logging frameworks, such as java.util.logging, logback and log4j

来自 官网的解释,从上面我们可以得知SLF4J缩写的由来,以及SLF4J可作为各种日志记录框架(例如java.util.logging,logback和log4j)的简单门面和抽象实现,通俗来说他就是一个门面类,让我们能够不关心内部日志各个子系统是如何互相调用,简化了我们的操作。

hello world

了解一样新事物,我们总是离不开helloworld,所以学习日志也是一样,我们从一个小例子开始吧!

首先,新建一个maven项目,在pom.xml中引入:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.28</version>
</dependency>

新建一个类

public class HelloLog {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(HelloLog.class);
        logger.info("Hello World");
    }
}

执行main方法,我们可以在控制台看到如下输出:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

这是因为没有找到可以绑定到slf4j的实现。关于slf4j的实现其实有不少,

比如logback,log4j,slf4j-simple等等

这里我们给他引入一个他的具体实现

        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>

这时候我们就能在控制台看到具体的日志输出了

21:49:34.701 [main] INFO com.exercise.HelloLog - Hello World

如果你认真看maven的关系图的话你就会发现,当你引入了上面的logback的jar包之后其实也已经有了slf4j,还有logback-core基础包,这里我们先一笔带过

包的引用
包的引用

源码解读

让我们从源码层级来解读这个流程:

首先进入getLogger方法,看看他到底在作甚

LoggerFactory 通过静态方法 getLogger(Class clazz) 获取 Logger

-》方法内部又通过 getLogger(String name) 方法获取 Logger

Logger logger = LoggerFactory.getLogger(HelloLog.class);
//LoggerFactory.java
public static Logger getLogger(Class<?> clazz) {
        //核心代码,绑定神马的都在这里面
        Logger logger = getLogger(clazz.getName());
        if (DETECT_LOGGER_NAME_MISMATCH) {
            Class<?> autoComputedCallingClass = Util.getCallingClass();
            if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
                Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
                                autoComputedCallingClass.getName()));
                Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
            }
        }
        return logger;
    }

getLogger方法又在做什么呢?getLogger(String name) 方法使用同类中返回类型为 ILoggerFactory getILoggerFactory() 方法获取 ILoggerFactory 实例

ILoggerFactory 是获得 Logger 的工厂接口,接口定义了获得 Logger 的方法 getLogger()

public static Logger getLogger(String name) {
        ////进入 getILoggerFactory 方法
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        return iLoggerFactory.getLogger(name);
    }

getILoggerFactory();方法,我们可以观察到他有一些静态变量的赋值,比较操作,让我们看一下这些静态变量定义了啥,是干啥的。

LoggerFactory 定义了若干静态变量,代码如下

  • 其中定义了 5个 静态不可变 int 类型的变量,

用于表示 LoggerFactory 初始化过程的不同状态值

    • UNINITIALIZED 未初始化
    • ONGOING_INITIALIZATION 正在被初始化
    • FAILED_INITIALIZATION 初始化失败
    • SUCCESSFUL_INITIALIZATION 初始化成功
    • NOP_FALLBACK_INITIALIZATION 空状态,表示无底层日志实现框架时的结果
    • volatile 变量 INITIALIZATION_STATE,用于标记 ILoggerFactory 实例初始化的结果,且初始化为 UNINITIALIZED

    从上面我们就可以知道了,原来这些静态变量都是为了记录状态用的,关于volatile我们后续再开一片文章细讲,这里我们只要知道他可以起到变量改变后,变量在线程之间的可见性,类似轻量级同步。

    通过 synchronized 代码块初始化 INITIALIZATION_STATE

     /**
         * Return the {@link ILoggerFactory} instance in use.
         * <p/>
         * <p/>
         * ILoggerFactory instance is bound with this class at compile time.
         * 
         * @return the ILoggerFactory instance in use
         */
        public static ILoggerFactory getILoggerFactory() {
        //类加载时, INITIALIZATION_STATE == UNINITIALIZED,标志为未初始化
        //且对 初始化状态的更新使用了 双重校验锁(DCL,即 double-checked locking),参考单例模式初始化
            if (INITIALIZATION_STATE == UNINITIALIZED) {
                synchronized (LoggerFactory.class) {
                    if (INITIALIZATION_STATE == UNINITIALIZED) {
                        //将初始化状态标记为 ONGOING_INITIALIZATION
                        INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                         //执行 ILoggerFactory 实例初始化,实际是开始在 classpath 寻找看看是否存在 。。。
                    //我们先跳到初始化的位置,这也是Slf4j 实现 facade 的核心所在
                    //了解了核心,再转回来看下面的
                        performInitialization();
                    }
                }
            }
            switch (INITIALIZATION_STATE) {
            case SUCCESSFUL_INITIALIZATION:
                return StaticLoggerBinder.getSingleton().getLoggerFactory();
            case NOP_FALLBACK_INITIALIZATION:
                return NOP_FALLBACK_FACTORY;
            case FAILED_INITIALIZATION:
                throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
            case ONGOING_INITIALIZATION:
                // support re-entrant behavior.
                // See also http://jira.qos.ch/browse/SLF4J-97
                return SUBST_FACTORY;
            }
            throw new IllegalStateException("Unreachable code");
        }

    当 INITIALIZATION_STATE == UNINITIALIZED 未初始化 ,performInitialization() 方法进而调用 bind() 方法初始化 INITIALIZATION_STATE

    private final static void performInitialization() {
        //核心代码 bind, 开始绑定 ILoggerFactory 具体实现
        bind();
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            versionSanityCheck();
        }
    }

    bind() 方法用于初始化 INITIALIZATION_STATE

        private final static void bind() {
            try {
                //存储符合的绑定路径path
                Set<URL> staticLoggerBinderPathSet = null;
                // skip check under android, see also
                // http://jira.qos.ch/browse/SLF4J-328
                if (!isAndroid()) {
                    //绑定的过程,该方法去寻找 StaticLoggerBinder.class 文件
                    staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                    reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
                }
                // the next line does the binding
                //寻找 StaticLoggerBinder 类,若找不到则抛异常
                //pom 没有添加 日志实现依赖,抛异常
                StaticLoggerBinder.getSingleton();
                INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
                // 如果classpath中存在多个slf4j binding,则在此打印出最终使用的binding
                reportActualBinding(staticLoggerBinderPathSet);
                fixSubstituteLoggers();
                replayEvents();
                // release all resources in SUBST_FACTORY
                SUBST_FACTORY.clear();
            } catch (NoClassDefFoundError ncde) {
             // 如果classpath不存在任何slf4j binding,则找不到StaticLoggerBinder类
            // 会抛出NoClassDefFoundError,这捕获改异常,如果没有找到binding,则使用NOPLogger
                String msg = ncde.getMessage();
                if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
                    INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
                    Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
                    Util.report("Defaulting to no-operation (NOP) logger implementation");
                    Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
                } else {
                    failedBinding(ncde);
                    throw ncde;
                }
            } catch (java.lang.NoSuchMethodError nsme) {
                String msg = nsme.getMessage();
                if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
                    INITIALIZATION_STATE = FAILED_INITIALIZATION;
                    Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
                    Util.report("Your binding is version 1.5.5 or earlier.");
                    Util.report("Upgrade your binding to version 1.6.x.");
                }
                throw nsme;
            } catch (Exception e) {
                failedBinding(e);
                throw new IllegalStateException("Unexpected initialization failure", e);
            }
        }

    bind() 方法的关键代码是 **StaticLoggerBinder.getSingleton()**

    那么我们怎么知道要用哪个呢?是不是随便一个类这样命名就可以了呢?我们可以从当前类的import部分找到答案:

    import org.slf4j.impl.StaticLoggerBinder;

    该行代码会加载 org.slf4j.impl.StaticLoggerBinder.class ,恰好和findPossibleStaticLoggerBinderPathSet()方法中查找的一致。

    StaticLoggerBinder类是从哪里来的?我们看代码的时候,可以发现在slf4j-api的源代码中,的确有对应的package和类存在。但是打开打包好的slf4j-api.jar,却发现根本没有这个implpackage。

    源码中的StaticLoggerBinder
    源码中的StaticLoggerBinder

    而且这个StaticLoggerBinder类的代码也明确说这个类不应当被打包到slf4j-api.jar:

    private StaticLoggerBinder() {
        throw new UnsupportedOperationException(
            "This code should have never made it into slf4j-api.jar");
    }

    在slf4j-api项目的pom.xml文件中,我们可以找到下面的内容:

          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-antrun-plugin</artifactId>
            <executions>
              <execution>
                <phase>process-classes</phase>
                <goals>
                 <goal>run</goal>
                </goals>
              </execution>
            </executions>
            <configuration>
              <tasks>
                <echo>Removing slf4j-api's dummy StaticLoggerBinder and StaticMarkerBinder</echo>
                <delete dir="target/classes/org/slf4j/impl"/>
              </tasks>
            </configuration>
          </plugin>
        </plugins>
    

    这里通过调用ant在打包为jar文件前,将package org.slf4j.impl和其下的class都删除掉了。

    实际上这里的impl package内的代码,只是用来占位以保证可以编译通过。需要在运行时再进行绑定。所以当你只引入slf4j没有引入绑定的具体实现会抛出NoClassDefFoundError异常进入该异常的catch处理。

    INITIALIZATION_STATE 的赋值动作发生在该条代码的执行成功和发生异常时

    • 正常执行时

      • INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION
    • org.slf4j.impl.StaticLoggerBinder.class 未找到时,即进入 NoClassDefFoundError catch语句时

      • INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION
    • org.slf4j.impl.StaticLoggerBinder.class 可以找到, 但未定义 getSingleton() 方法时,即进入 java.lang.NoSuchMethodError catch 语句时

      • INITIALIZATION_STATE = FAILED_INITIALIZATION
    • 在对 INITIALIZATION_STATE 初始化前,bind 方法先会查找org.slf4j.impl.StaticLoggerBinder.class 类路径,并先保存到集合 Set 中

      • 方法 reportMultipleBindingAmbiguity(staticLoggerBinderPathSet) 在 Set size > 1 时告知 client 此时包括多个 bindings
      • 方法 reportActualBinding(staticLoggerBinderPathSet) Set size > 1 告知client 实际绑定的是哪一个 bindings

    如果出现了找到多个实现那么JVM会挑一个实现来用,这种情况下,会使用更靠前的那个类,因为JVM是从前往后搜索类的。

    我们再回头看findPossibleStaticLoggerBinderPathSet这个方法

    // We need to use the name of the StaticLoggerBinder class, but we can't
    // reference
    // the class itself.
    //所有日志实现框架的包路径及 所必需包含的 StaticLoggerBinder 类路径
    private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
    
    static Set<URL> findPossibleStaticLoggerBinderPathSet() {
        // use Set instead of list in order to deal with bug #138
        // LinkedHashSet appropriate here because it preserves insertion order
        // during iteration
        Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
        try {
            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            Enumeration<URL> paths;
             //而用户在运行期,是获取不到引导类加载器bootstrapclassloader的,因此当一个类获取它的类加载器,得到的对象时null,就说明它是由引导类加载器加载的。
            //引导类加载器是负责加载系统目录下的文件,因此源码中使用getSystemresource来获取资源文件
            if (loggerFactoryClassLoader == null) {
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
             //判断能否找到 StaticLoggerBinder 的 class 文件
                //pom 未添加 日志实现 依赖包的话 是找不到该 class 文件的
                //因为可能存在若干个该 class 文件,故此处用 Enumeration 来迭代存储URL,Enumeration 现在被 Iteration 替代
                //两者功能一致
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }
            while (paths.hasMoreElements()) {
                URL path = paths.nextElement();
                staticLoggerBinderPathSet.add(path);
            }
        } catch (IOException ioe) {
            Util.report("Error getting resources from path", ioe);
        }
        return staticLoggerBinderPathSet;
    }

    findPossibleStaticLoggerBinderPathSet通过类加载器找到StaticLoggerBinder.class

    JVM平台提供三种类加载器

    • Bootstrap ClassLoader
      启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
    • Extension ClassLoader
      扩展类加载器是指Sun公司实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
    • System ClassLoader
      系统类加载器,也称应用程序加载器,是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,即加载由System.getProperty("java.class.path")指定目录下的文件,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

    这里我们需要涉及到双亲委派模式,他的原理:当一个类加载器接收到类加载请求时,首先会请求其父类加载器加载,每一层都是如此,当父类加载器无法找到这个类时(根据类的全限定名称),子类加载器才会尝试自己去加载。
    用户在运行期,也是获取不到引导类加载器的,因此当一个类获取它的类加载器,得到的对象时null,就说明它是由引导类加载器加载的。引导类加载器是负责加载系统目录下的文件,因此源码中使用getSystemresource来获取资源文件。

    类加载器
    类加载器

    由于slf4j-api项目并未加入日志实现框架

    • classpath 下找不到StaticLoggerBinder.class
    • StaticLoggerBinder.getSingleton() 将会抛出 NoClassDefFoundError 异常
    • 初始化 INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION
    • switch 语句,当初始化状态为 NOP_FALLBACK_INITIALIZATION 时,返回 NOP_FALLBACK_FACTORY, NOP_FALLBACK_FACTORY 是 LoggerFactory 下 slf4j-api 中一个 NOPLoggerFactory 类对象
    public static Logger getLogger(String name) {
        //classpath 不存在日志实现框架的,此刻得到的是 NOPLoggerFactory 的实例
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        //进入NOPLoggerFactory 的 getLogger 
        return iLoggerFactory.getLogger(name);
    }

    通过 NopLoggerFactory 可以获取slf4j 自带的 NOPLogger 对象

    public class NOPLoggerFactory implements ILoggerFactory {
    
        public NOPLoggerFactory() {
            // nothing to do
        }
        
        //获取具体日志对象
        public Logger getLogger(String name) {
            return NOPLogger.NOP_LOGGER;
        }
    
    }

    那我们具体看一下NOPLogger类,发现这个类都没用具体的实现,所以如果没有绑定到依赖就会打不出日志,并且打印出警告信息。也就出现了开头的控制台中看到的警告。

    public class NOPLogger extends MarkerIgnoringBase {
    
        private static final long serialVersionUID = -517220405410904473L;
    
        /**
         * The unique instance of NOPLogger.
         */
        public static final NOPLogger NOP_LOGGER = new NOPLogger();
    
        /**
         * There is no point in creating multiple instances of NOPLogger,
         * except by derived classes, hence the protected  access for the constructor.
         */
        protected NOPLogger() {
        }
    
        /**
         * Always returns the string value "NOP".
         */
        public String getName() {
            return "NOP";
        }
    
        /** A NOP implementation. */
        final public void debug(String msg) {
            // NOP
        }
    
        /** A NOP implementation.  */
        final public void debug(String format, Object arg) {
            // NOP
        }
    
        /** A NOP implementation. */
        final public void info(String msg) {
            // NOP
        }
    
        /** A NOP implementation. */
        final public void info(String format, Object arg1) {
            // NOP
        }
    
        /** A NOP implementation. */
        final public void info(String msg, Throwable t) {
            // NOP
        }
    
    
    }

    假如我们成功执行了bind()绑定了一个实现,我们再回到这个方法中

     private final static void performInitialization() {
        bind();
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
        // 绑定后的版本兼容性检查
          versionSanityCheck();
        }
      }

    我们可以知道绑定成功后还要做一次版本兼容性校验

        private final static void versionSanityCheck() {
            try {
                String requested = StaticLoggerBinder.REQUESTED_API_VERSION;
    
                boolean match = false;
                for (String aAPI_COMPATIBILITY_LIST : API_COMPATIBILITY_LIST) {
                    if (requested.startsWith(aAPI_COMPATIBILITY_LIST)) {
                        match = true;
                    }
                }
                if (!match) {
                    Util.report("The requested version " + requested + " by your slf4j binding is not compatible with "
                                    + Arrays.asList(API_COMPATIBILITY_LIST).toString());
                    Util.report("See " + VERSION_MISMATCH + " for further details.");
                }
            } catch (java.lang.NoSuchFieldError nsfe) {
                // given our large user base and SLF4J's commitment to backward
                // compatibility, we cannot cry here. Only for implementations
                // which willingly declare a REQUESTED_API_VERSION field do we
                // emit compatibility warnings.
            } catch (Throwable e) {
                // we should never reach here
                Util.report("Unexpected problem occured during version sanity check", e);
            }
        }

    它会取实现类中的REQUESTED_API_VERSION常量字段来和当前slf4j版本中定义的API_COMPATIBILITY_LIST字段比较,比较大版本号,如果大版本相同则通过,如果不相同,那么进行失败提示。通常实现类的每个发行版都会修改这个字段。

    典型使用

    public class TypicalUsage {
        private static final Logger LOGGER = LoggerFactory.getLogger(TypicalUsage.class);
    
        private static String param;
    
        private static Integer num;
    
        public static void setParam(String inParam, Integer inNum) {
            param = inParam;
            num = inNum;
            LOGGER.debug("inParam is {}. num is {}.", param, num);
    
            if (num > 50) {
                LOGGER.info("num  is more than  50 .");
            }
        }
    
        public static void main(String[] args) {
            setParam("hello",100);
        }
    }

    输出:

    22:03:48.074 [main] DEBUG com.exercise.TypicalUsage - inParam is hello. num is 100.
    22:03:48.082 [main] INFO com.exercise.TypicalUsage - num  is more than  50 .

    我们使用 {}作为占位符,实现参数的打印。

    一般来说我们组装日志可能会想到说使用如下的字符串拼接方式:

    logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));

    但是这无疑产生了一些额外的开销,例如将整数转字符串,或者方法的执行转换以及字符串的组装拼接,那么日志打印的效率必然会因此受到一些影响。有时候我们可能只需要将日志级别调整到info级别进行打印,但是debug的日志语句横亘其中,即使你是不需要记录他的但是仍然会执行这条语句的一些方法。slf4j提供了一种这样的方式来避免这些可能产生的高成本但是又不一定需要打印日志的行为。另一方面,如果为日志记录启用了DEBUG级别,则将产生两次是否记录日志的成本:一次在debugEnabled中,一次在debug中。 但这是微不足道的开销,因为这个判断所花费的时间不到实际记录一条语句的时间的1%。

    if(logger.isDebugEnabled()) {
      logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
    }

    而这是优化的一小步,想要再更好优化执行时间,我们就要考虑打印信息的组装了。

    Mapped Diagnostic Context (MDC) support

    "Mapped Diagnostic Context" is essentially a map maintained by the logging framework where the application code provides key-value pairs which can then be inserted by the logging framework in log messages. MDC data can also be highly helpful in filtering messages or triggering certain actions.

    SLF4J supports MDC, or mapped diagnostic context. If the underlying logging framework offers MDC functionality, then SLF4J will delegate to the underlying framework's MDC. Note that at this time, only log4j and logback offer MDC functionality. If the underlying framework does not offer MDC, for example java.util.logging, then SLF4J will still store MDC data but the information therein will need to be retrieved by custom user code.

    Thus, as a SLF4J user, you can take advantage of MDC information in the presence of log4j or logback, but without forcing these logging frameworks upon your users as dependencies.

    For more information on MDC please see the chapter on MDC in the logback manual.

    上述是来自官方的说明,大意是MDC是各自的日志记录框架维护的映射,其中应用程序代码提供键值对,然后可以由日志记录框架将其插入日志消息中。MDC数据在筛选消息或触发某些操作方面也可能非常有帮助。比如我们可以利用MDC来生成当前请求的唯一requestId,然后打印在日志里,帮助我们过滤无用的日志信息,只专注于当前的请求。

    SLF4J支持MDC或映射的诊断上下文。 如果基础日志框架提供MDC功能,则SLF4J将委派给基础框架的MDC。

    只有log4j和logback提供MDC功能。 如果基础框架不提供MDC(例如java.util.logging),则SLF4J仍将存储MDC数据,但是其中的信息将需要由自定义用户代码检索。

    因此,作为SLF4J用户,您可以在存在log4j或logback的情况下利用MDC信息,但无需将这些日志框架强制依赖于用户。

    这个部分我们再开一篇文章和logback一起解读。

    SLF4J 2.0

    slf4j 2.0采用了新的设计,使用一个LoggingEventBuilder对象逐段构建日志事件,并在事件完全组装完毕之后进行日志的记录。他有atTrace(), atDebug(), atInfo(), atWarn() and atError()一系列方法。那你可能会问,哎呀,比如我设置日志级别是info,但是我也会记录一些debug级别的日志,会不会一起输出,答案是不会的,碰到不需要记录的日志级别就会跳过,所以它跟传统的日志记录都能用纳秒级别的性能。

    示例:

    logger.atInfo().log("Hello world");

    等效于

    logger.info("Hello world.");
            int newT = 15;
            int oldT = 16;
    
            // using traditional API
            logger.debug("Temperature set to {}. Old temperature was {}.", newT, oldT);
    
            // using fluent API, add arguments one by one and then log message
            logger.atDebug().addArgument(newT).addArgument(oldT).log("Temperature set to {}. Old temperature was {}.");
    
            // using fluent API, log message with arguments
            logger.atDebug().log("Temperature set to {}. Old temperature was {}.", newT, oldT);
    
            // using fluent API, add one argument and then log message providing one more argument
            logger.atDebug().addArgument(newT).log("Temperature set to {}. Old temperature was {}.", oldT);
    
            // using fluent API, add one argument with a Supplier and then log message with one more argument.
            // Assume the method t16() returns 16.
            logger.atDebug().addArgument(() -> t16()).log(msg, "Temperature set to {}. Old temperature was {}.", oldT);

    还可以使用键值对

            int newT = 15;
            int oldT = 16;
    
            // using classical API
            logger.debug("oldT={} newT={} Temperature changed.", newT, oldT);
    
            // using fluent API
            logger.atDebug().addKeyValue("oldT", oldT).addKeyValue("newT", newT).log("Temperature changed.");          
          

    SLF4J警告以及报错信息代表的含义

    官方wiki

    参考:

    http://techblog.ppdai.com/2018/07/04/20180704/

    https://skyao.github.io/2014/07/21/slfj4-binding/

    https://www.cnblogs.com/xing901022/p/4149524.html

    https://juejin.im/post/5c866e00f265da2dd1689f8b

    https://www.ibm.com/developerworks/cn/java/j-lo-classloader/

    http://imushan.com/2018/07/22/java/language/Java%E6%97%A5%E5%BF%97-SLF4J%E4%BD%BF%E7%94%A8%E4%B8%8E%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/

    http://www.slf4j.org/manual.html