log4j與logback沖突的解決與思考

問題

在項目啟動時居砖,發(fā)現(xiàn)打印了大量的debug日志曲梗,但是src/main/resources下明明有log4j.xml夸溶,而且日志級別還設(shè)置的是info矾策,為什么會打印出大量的debug日志呢?

分析

使用eclipse的Dependency Hierarchy功能可以對pom.xml中定義的jar依賴進行可視化展示悠咱。發(fā)現(xiàn)項目中與日志相關(guān)的jar包有:

slf4j-api.jar
slf4j-log4j12.jar
log4j.jar
logback-classic.jar
logback-core.jar

那么蒸辆,猜測原因大概是:由于意外地引入了的logback的jar包,破壞了之前的slf4j + slf4j-log4j12 + log4j 的日志架構(gòu)析既,使得日志不再通過 log4j 輸出(因而log4j.xml設(shè)置的info日志級別也隨之失效)躬贡,而是通過 logback 輸出,并且logback 默認輸出的是debug級別眼坏,因此出現(xiàn)了上述問題拂玻。

解決

解決的方法很簡單:在pom.xml中通過<exclusions/>排除掉 logback 相關(guān)的jar即可。

思考

對于這個小問題宰译,解決起來似乎很簡單檐蚜,但是以下幾個問題是值得我們進一步思考的:

基于 slf4j + slf4j-log4j12 + log4j 的日志架構(gòu),是如何工作的沿侈?
為什么引入了 logback 就會破壞之前的日志架構(gòu)闯第?
如何避免發(fā)生log4j和logback沖突的問題?
面向slf4j的接口API編程的最佳實踐是什么缀拭?

下面就來嘗試回答上面的幾個問題咳短。

基于 slf4j + slf4j-log4j12 + log4j 的日志架構(gòu)填帽,是如何工作的?

為了更好的了解其運行原理咙好,我搭建一個簡單的項目篡腌。見:https://github.com/wanghui0101/learning-slf4j/tree/master/learning-slf4j-sample

/learning-slf4j-sample/src/main/webapp/WEB-INF/web.xml中只簡單配置了一個Servlet,指向personal.wh.learning.slf4j.sample.SampleServlet類敷扫。此類的源碼為:

package personal.wh.learning.slf4j.sample;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SampleServlet extends HttpServlet {

    private static final long serialVersionUID = -870284320856607145L;
    
    private static final Logger logger = LoggerFactory.getLogger(SampleServlet.class);
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        logger.debug("Hello Slf4j ! - debug");
        logger.info("Hello Slf4j ! - info");
        try (PrintWriter writer = resp.getWriter()) {
            writer.write("ok");
        }
    }
    
}

功能很簡單哀蘑,GET請求這個Servlet的時候,通過slf4j的org.slf4j.Logger加載了log4j葵第,并讀取log4j.xml绘迁,實現(xiàn)了日志的打印。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN" "log4j.dtd">
<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
    <appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %-5p [%t] %C{2} (%F:%L) - %m%n" />
        </layout>
    </appender>
    <root>
        <priority value="info" />
        <appender-ref ref="STDOUT" />
    </root>
</log4j:configuration>

由于日志級別為info卒密,因此只打印一句Hello Slf4j ! - info缀台。

項目的lib包,與日志相關(guān)的有:

slf4j-api-1.7.25.jar
slf4j-log4j12-1.7.25.jar
log4j-1.2.17.jar(通過 slf4j-log4j12-1.7.25.jar 傳遞依賴引入)

源碼分析

接下來哮奇,我們通過源碼分析一下膛腐,slf4j是如何加載log4j,并實現(xiàn)日志打印的呢鼎俘?

org.slf4j.LoggerFactory.getLogger()

我們從org.slf4j.LoggerFactory#getLogger(Class<?> clazz)開始:

public static Logger getLogger(Class<?> clazz) {
    Logger logger = getLogger(clazz.getName()); // 通過類名獲取Logger對象
    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; // 返回Logger對象
}

然后org.slf4j.LoggerFactory#getLogger(String name)

public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory(); // 重點
    return iLoggerFactory.getLogger(name);
}

在查看getILoggerFactory()之前哲身,先看一下org.slf4j.ILoggerFactory接口的定義:

public interface ILoggerFactory {
    public Logger getLogger(String name); // 根據(jù)Logger的名字返回Logger對象
}

再看一下org.slf4j.Logger接口的定義(只列出主要方法):

public interface Logger {
    public String getName(); // 返回logger實例的名稱
    public void trace(String format, Object... arguments);
    public void debug(String format, Object... arguments);
    public void info(String format, Object... arguments);
    public void warn(String format, Object... arguments);
    public void error(String format, Object... arguments);
    public boolean isTraceEnabled();
    public boolean isDebugEnabled();
    public boolean isInfoEnabled();
    public boolean isWarnEnabled();
    public boolean isErrorEnabled();
}

就是打印各級別的日志,以及判斷當前的日志級別贸伐。

好勘天,繼續(xù)查看org.slf4j.LoggerFactory#getILoggerFactory()的源碼

public static ILoggerFactory getILoggerFactory() {
    if (INITIALIZATION_STATE == UNINITIALIZED) { // 是否是未初始化狀態(tài)?注意volatile關(guān)鍵字的使用
        // 同步鎖捉邢,所有線程互斥脯丝,只允許1個線程進行下面的初始化操作
        synchronized (LoggerFactory.class) {
            if (INITIALIZATION_STATE == UNINITIALIZED) { // 雙重檢查
                INITIALIZATION_STATE = ONGOING_INITIALIZATION; // 設(shè)置狀態(tài)為進行中
                performInitialization(); // 執(zhí)行初始化
            }
        }
    }
    switch (INITIALIZATION_STATE) { // 判斷初始化狀態(tài)
        case SUCCESSFUL_INITIALIZATION: // 成功初始化
            // 返回StaticLoggerBinder中定義的ILoggerFactory的實現(xiàn)類
            return StaticLoggerBinder.getSingleton().getLoggerFactory();
        case NOP_FALLBACK_INITIALIZATION: // 未找到StaticLoggerBinder類
            // 返回?zé)o操作(即所有實現(xiàn)的都是空)的org.slf4j.helpers.NOPLoggerFactor
            return NOP_FALLBACK_FACTORY; 
        case FAILED_INITIALIZATION: // 初始化失敗,拋異常
            throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
        case ONGOING_INITIALIZATION: // 仍是進行中的狀態(tài)
            return SUBST_FACTORY;
    }
    // 如果以上的狀態(tài)都不是伏伐,則拋異常
    throw new IllegalStateException("Unreachable code");
}

核心代碼是org.slf4j.LoggerFactory#performInitialization()方法:

private final static void performInitialization() {
    bind(); // 與具體的日志實現(xiàn)綁定
    if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) { // 如果成功初始化
        versionSanityCheck(); // 對版本兼容性的檢查宠进,不再看了
    }
}

繼續(xù)org.slf4j.LoggerFactory#bind()

private final static void bind() {
    try {
        Set<URL> staticLoggerBinderPathSet = null;
        if (!isAndroid()) { // 我們當前并不是Android環(huán)境,會執(zhí)行if語句塊的內(nèi)容
            // 查找所有StaticLoggerBinder類的路徑
            staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
            // 如果找到了多個StaticLoggerBinder類藐翎,并且不確定使用哪個時材蹬,會打印錯誤信息
            reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
        }
        // 最關(guān)鍵的一句代碼
        // 首先看StaticLoggerBinder類的路徑是 org.slf4j.impl.StaticLoggerBinder
        // 注意:它并不在 slf4j-api.jar 中,而是在 slf4j-log4j12.jar 中
        // 即:slf4j-api.jar 中定義的是高層接口吝镣,實現(xiàn)與具體日志框架綁定是在 slf4j-[日志框架].jar 中
        // 如果項目中只有 slf4j-api.jar 的話堤器,那么就會因為找不到 StaticLoggerBinder 
        // 而進入到 catch (NoClassDefFoundError ncde) 代碼塊
        // 此句代碼確保了一定要找到StaticLoggerBinder類,并且能夠執(zhí)行g(shù)etSingleton()方法
        StaticLoggerBinder.getSingleton();
        
        // 找到了StaticLoggerBinder赤惊,表示初始化成功
        INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;

        // 向控制臺打印實際綁定的日志框架
        reportActualBinding(staticLoggerBinderPathSet);
        
        ......
 
    // 如果找不到StaticLoggerBinder類,會執(zhí)行以下代碼
    } catch (NoClassDefFoundError ncde) {
        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;
        }

    // 如果能找StaticLoggerBinder類凰锡,但是找不到getSingleton()方法未舟,會執(zhí)行以下代碼
    } 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);
    }
}

小結(jié)一下:此方法的實現(xiàn)就是找org.slf4j.impl.StaticLoggerBinder的過程圈暗。接下來關(guān)注一下具體的過程。

  1. org.slf4j.LoggerFactory#findPossibleStaticLoggerBinderPathSet()
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";

static Set<URL> findPossibleStaticLoggerBinderPathSet() {
    Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>(); // 注意:是有序的Set
    try {
        /* 
         * 通過ClassLoader加載所有的 org/slf4j/impl/StaticLoggerBinder.class
         */
        ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
        Enumeration<URL> paths;
        if (loggerFactoryClassLoader == null) {
            paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
        } else {
            paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
        }
        while (paths.hasMoreElements()) { // 可能有多個
            URL path = paths.nextElement();
            staticLoggerBinderPathSet.add(path); // 添加到Set中
        }
    } catch (IOException ioe) {
        Util.report("Error getting resources from path", ioe);
    }
    return staticLoggerBinderPathSet; // 返回Set
}

org/slf4j/impl/StaticLoggerBinder.class可能有多個裕膀,但是目前這個類只在 slfj4-log4j12.jar 中有1個员串。

  1. org.slf4j.LoggerFactory#reportMultipleBindingAmbiguity(Set<URL>)
private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
    // 找到的這些StaticLoggerBinder是否是有歧義的?
    if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
        Util.report("Class path contains multiple SLF4J bindings.");
        for (URL path : binderPathSet) { // 如果找到多個則打印錯誤信息
            Util.report("Found binding in [" + path + "]");
        }
        // 打印一個網(wǎng)址昼扛,用于解釋多個綁定的情況:https://www.slf4j.org/codes.html#multiple_bindings
        Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
    }
}

private static boolean isAmbiguousStaticLoggerBinderPathSet(Set<URL> binderPathSet) {
    return binderPathSet.size() > 1; // 找到多個StaticLoggerBinder就是有歧義的
}

由于目前只找到1個寸齐,所以沒有歧義。

  1. org.slf4j.LoggerFactory.reportActualBinding(Set<URL>)
private static void reportActualBinding(Set<URL> binderPathSet) {
    // 當有歧義時抄谐,打印出實際綁定的那個StaticLoggerBinder
    if (binderPathSet != null && isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
        Util.report("Actual binding is of type [" + StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr() + "]");
    }
}

至此渺鹦,綁定過程我們就分析完了。簡而言之蛹含,就是:查找并加載StaticLoggerBinder的過程毅厚。

org.slf4j.impl.StaticLoggerBinder

接下來,我們看一下 slf4j-log4j12.jar 中的 org.slf4j.impl.StaticLoggerBinder浦箱。

package org.slf4j.impl;

import org.apache.log4j.Level;
import org.slf4j.ILoggerFactory;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.Util;
import org.slf4j.spi.LoggerFactoryBinder;

public class StaticLoggerBinder implements LoggerFactoryBinder {

    private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

    public static final StaticLoggerBinder getSingleton() { // 構(gòu)建單例
        return SINGLETON;
    }

    public static String REQUESTED_API_VERSION = "1.6.99"; // !final

    private static final String loggerFactoryClassStr = Log4jLoggerFactory.class.getName();

    private final ILoggerFactory loggerFactory;

    private StaticLoggerBinder() {
        loggerFactory = new Log4jLoggerFactory(); // 重點
        try {
            @SuppressWarnings("unused")
            Level level = Level.TRACE;
        } catch (NoSuchFieldError nsfe) {
            Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
        }
    }

    @Override
    public ILoggerFactory getLoggerFactory() {
        return loggerFactory; // 返回 Log4jLoggerFactory 對象
    }

    @Override
    public String getLoggerFactoryClassStr() {
        return loggerFactoryClassStr;
    }
}

接下來吸耿,我們來看org.slf4j.impl.Log4jLoggerFactory的源碼(有刪減)

package org.slf4j.impl;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.log4j.LogManager;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;

// 實現(xiàn)了 slf4j-api.jar 中的 ILoggerFactory  接口
public class Log4jLoggerFactory implements ILoggerFactory {

    // key - logger的名字
    // value - org.slf4j.Logger接口的實現(xiàn)類,是個適配器 org.slf4j.impl.Log4jLoggerAdapter
     // 考慮并發(fā)的情況酷窥,使用了 ConcurrentMap
    ConcurrentMap<String, Logger> loggerMap;

    public Log4jLoggerFactory() {
        loggerMap = new ConcurrentHashMap<String, Logger>();
        // force log4j to initialize
        org.apache.log4j.LogManager.getRootLogger();
    }

    @Override
    public Logger getLogger(String name) {
        Logger slf4jLogger = loggerMap.get(name);
        if (slf4jLogger != null) { // 找到直接返回
            return slf4jLogger;
        } else {
            // 先獲取log4j的Logger對象
            org.apache.log4j.Logger log4jLogger;
            if (name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME))
                log4jLogger = LogManager.getRootLogger();
            else
                log4jLogger = LogManager.getLogger(name);

            // 通過 Log4jLoggerAdapter 來將log4j的Logger適配為slf4j的Logger
            Logger newInstance = new Log4jLoggerAdapter(log4jLogger);
            Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
            return oldInstance == null ? newInstance : oldInstance;
        }
    }
}

邏輯很清楚咽安,接下來的重點就是Log4jLoggerAdapter,這個典型的適配器模式的實現(xiàn)(有刪減):

package org.slf4j.impl;

// org.slf4j.spi.LocationAwareLogger 繼承了 org.slf4j.Logger 接口
public final class Log4jLoggerAdapter extends MarkerIgnoringBase implements 
    LocationAwareLogger, Serializable {

    final transient org.apache.log4j.Logger logger;

    Log4jLoggerAdapter(org.apache.log4j.Logger logger) {
        this.logger = logger;
    }
    
    // 實現(xiàn) org.slf4j.Logger 接口中的方法
    @Override
    public void info(String format, Object... argArray) {
        if (logger.isInfoEnabled()) {
            FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray);
            // 實際是通過log4j的API進行打印日志
            logger.log(FQCN, Level.INFO, ft.getMessage(), ft.getThrowable());
        }
    }

    ......
}

通過適配器實現(xiàn)了:當我們編程時面向的是org.slf4j.Logger蓬推,而實際執(zhí)行是通過org.apache.log4j.Logger執(zhí)行打印日志等操作妆棒。

總結(jié)

對 slf4j + slf4j-log4j12 + log4j 的運行原理進行一下總結(jié):

  1. org.slf4j.LoggerFactory.getLogger(Class)方法的邏輯就是通過ClassLoader查找所有的org.slf4j.impl.StaticLoggerBinder.class,并確定最終實際使用的那一個拳氢。
  2. StaticLoggerBinder是個單例募逞,并實現(xiàn)了LoggerFactoryBinder接口,用以實現(xiàn)slf4j與日志框架實現(xiàn)之間的綁定馋评。它可以返回一個ILoggerFactory接口的實現(xiàn)類放接,在 slf4j-log4j12.jar 中是Log4jLoggerFactory
  3. Log4jLoggerFactory中必須返回一個org.slf4j.Logger接口的實現(xiàn)類留特,這個類是Log4jLoggerAdapter
  4. Log4jLoggerAdapter是一個典型的適配器模式的實現(xiàn)纠脾,它的功能是將log4j的org.apache.log4j.Logger適配成org.slf4j.Logger
  5. 當執(zhí)行org.slf4j.Logger.info()方法時,實際的執(zhí)行者是Log4jLoggerAdapter.info()蜕青,內(nèi)部是通過log4j的API完成日志的打印苟蹈。

為什么引入了logback就會破壞之前的日志架構(gòu)?

通過上面的分析右核,我們知道StaticLoggerBinder是實現(xiàn)slf4j和日志輸出框架之間綁定的橋梁慧脱,但如果classpath下有多個StaticLoggerBinder會怎樣呢?
實驗代碼見:https://github.com/wanghui0101/learning-slf4j/tree/master/learning-slf4j-multiple-bindings
pom.xml中添加了logback的jar包贺喝。
此時菱鸥,訪問Servlet宗兼,控制臺會打印2條語句:

Hello Slf4j ! - debug
Hello Slf4j ! - info

并且,在項目啟動時氮采,在控制臺還會打印出5句錯誤信息:

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/xxx/learning-slf4j-multiple-bindings/WEB-INF/lib/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/xxx/learning-slf4j-multiple-bindings/WEB-INF/lib/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]

通過之前的源碼分析殷绍,我們可以得知
第1-4句是org.slf4j.LoggerFactory.reportMultipleBindingAmbiguity(Set<URL>)打印出來的。
第5句是org.slf4j.LoggerFactory.reportActualBinding(Set<URL>)打印出來的鹊漠。
意思就是在classpath下找到了多個StaticLoggerBinder主到,并自動選擇了logback的ContextSelectorStaticBinder。那么躯概,此時slf4j-log4j12.jar就不再起作用登钥,而log4j.xml也不會起作用了。這樣楞陷,就出現(xiàn)了文章最開始提出的問題中描述的情況怔鳖。
那么,為什么會先加載logback的StaticLoggerBinder呢固蛾?這是與ClassLoader的類加載機制雙親委派模型有關(guān)的结执。同一個ClassLoader對于同一個類只會加載一次,所以即使classpath下有多個StaticLoggerBinder.class艾凯,也只有1個會被加載献幔,而且在我們當前的環(huán)境下恰好加載的就是logback的StaticLoggerBinder
因此趾诗,對于slf4j來說蜡感,查找到多個StaticLoggerBinder.class并不會影響它的執(zhí)行,只是打印出警告信息而已恃泪。
更多關(guān)于此問題的解釋郑兴,見:http://www.slf4j.org/codes.html#multiple_bindings

如何避免類似的沖突?

由于slf4j-log4j12.jar或logback.jar中都包含org/slf4j/impl/StaticLoggerBinder.class贝乎,為了不讓slf4j加載時產(chǎn)生歧義情连,需要移除這兩者之一,使slf4j只與1個明確的日志實現(xiàn)綁定览效。
記兹匆ā:始終要保證,在classpath下锤灿,只有1個org/slf4j/impl/StaticLoggerBinder.class挽拔。

作為服務(wù)提供方,我們提供的jar如果依賴了日志相關(guān)的jar但校,應(yīng)該依賴哪些螃诅?

通過上面的分析,如作為服務(wù)提供方,依賴的日志jar包术裸,只包括slf4j-api.jar即可空执。多余的jar包(如slf4j-log4j12.jar或logback.jar),可能會引起服務(wù)使用方的日志架構(gòu)出現(xiàn)問題穗椅。

面向slf4j的接口API編程的最佳實踐是什么?

盡管可以通過 slf4j-api + slf4j-log4j12 + log4j 或 slf4j-api + logback 實現(xiàn)與底層日志實現(xiàn)框架的綁定奶栖,但這樣做的前提是項目中所有類匹表,以及依賴的所有jar,都是面向slf4j的API編程宣鄙。盡管我們可以約束項目組成員袍镀,保證所有類都是面向slf4j編程的,但項目所依賴的jar卻無法控制冻晤。

為此苇羡,slf4j還提供了橋接機制,將其它日志實現(xiàn)API鼻弧,通過slf4j设江,轉(zhuǎn)換成我們項目中綁定的日志實現(xiàn)。

舉例:Spring框架中使用了commons-logging作為了日志輸出框架攘轩,那么slf4j提供了jcl-over-slf4j.jar將commons-logging的實現(xiàn)先橋接到slf4j叉存,再通過logback輸出(如果你選擇的日志實現(xiàn)是logback的話)。但是度帮,要注意:如果使用了jcl-over-slf4j.jar歼捏,就必須排除commons-logging.jar,因為jcl-over-slf4j.jar中已經(jīng)包括了commons-logging中的所有類笨篷。

另外瞳秽,一個干凈的SpringBoot項目,為我們提供了使用slf4j的最佳實踐的范例率翅。根據(jù)

https://docs.spring.io/spring-boot/docs/1.5.8.RELEASE/reference/htmlsingle/#getting-started-maven-installation

可以搭建一個干凈的SpringBoot項目练俐。它引入的日志相關(guān)jar有

  • jcl-over-slf4j.jar // 將commons-logging橋接到slf4j

  • jul-to-slf4j.jar // 將java.util.logging橋接到slf4j

  • log4j-over-slf4j.jar // 將log4j的API橋接到slf4j

  • slf4j-api.jar // slf4j的API

  • logback-classic.jar // logback作為項目的日志實現(xiàn)框架

  • logback-core.jar

這些jar被定義在 spring-boot-starter-logging/pom.xml 中(

https://github.com/spring-projects/spring-boot/blob/v1.5.8.RELEASE/spring-boot-starters/spring-boot-starter-logging/pom.xml

),我們也可依此安聘,規(guī)范項目的日志框架痰洒。

更多

slf4j關(guān)于多次綁定的解釋:

http://www.slf4j.org/codes.html#multiple_bindings

slf4j橋接遺留的日志API:

https://www.slf4j.org/legacy.html

slf4j的用戶手冊:

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市浴韭,隨后出現(xiàn)的幾起案子丘喻,更是在濱河造成了極大的恐慌,老刑警劉巖念颈,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泉粉,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機嗡靡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門跺撼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人讨彼,你說我怎么就攤上這事歉井。” “怎么了哈误?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵哩至,是天一觀的道長。 經(jīng)常有香客問我蜜自,道長菩貌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任重荠,我火速辦了婚禮箭阶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘戈鲁。我一直安慰自己仇参,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布婆殿。 她就那樣靜靜地躺著冈敛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鸣皂。 梳的紋絲不亂的頭發(fā)上抓谴,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音寞缝,去河邊找鬼癌压。 笑死,一個胖子當著我的面吹牛荆陆,可吹牛的內(nèi)容都是我干的滩届。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼被啼,長吁一口氣:“原來是場噩夢啊……” “哼帜消!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起浓体,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤泡挺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后命浴,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體娄猫,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡贱除,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了媳溺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片月幌。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖悬蔽,靈堂內(nèi)的尸體忽然破棺而出扯躺,到底是詐尸還是另有隱情,我是刑警寧澤蝎困,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布缅帘,位于F島的核電站,受9級特大地震影響难衰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜逗栽,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一盖袭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧彼宠,春花似錦鳄虱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至摧冀,卻和暖如春倍踪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背索昂。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工建车, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人椒惨。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓缤至,卻偏偏與公主長得像,于是被迫代替她去往敵國和親康谆。 傳聞我的和親對象是個殘疾皇子领斥,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)沃暗,斷路器月洛,智...
    卡卡羅2017閱讀 134,659評論 18 139
  • 前言 在日志Logger漫談中提到了slf4j僅僅是作為日志門面,給用戶提供統(tǒng)一的API使用孽锥,而真正的日志系統(tǒng)的實...
    LNAmp閱讀 3,519評論 0 5
  • 寫Java也有一段時間了膊存,一直都有用slf4j log4j輸出日志的習(xí)慣。但是始終都是抱著“拿來主義”的態(tài)度,復(fù)制...
    Minimumy閱讀 1,389評論 1 7
  • 在應(yīng)用程序中添加日志記錄總的來說基于三個目的:監(jiān)視代碼中變量的變化情況隔崎,周期性的記錄到文件中供其他應(yīng)用進行統(tǒng)計分析...
    時待吾閱讀 4,988評論 0 6
  • 那年剛買座駕爵卒,我喜不自禁虚缎,立即合影后發(fā)給女友,來一句改天帶你去兜風(fēng)钓株,我正開心著與姐妹一起分享快樂实牡。哪知收到一句神回...
    長腿小將軍閱讀 526評論 0 0