log4j與logback沖突的解決與思考

問題

在項目啟動時,發現打印了大量的debug日志,但是src/main/resources下明明有log4j.xml,而且日志級別還設置的是info,為什么會打印出大量的debug日志呢?

分析

使用eclipse的Dependency Hierarchy功能可以對pom.xml中定義的jar依賴進行可視化展示。發現項目中與日志相關的jar包有:

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

那么,猜測原因大概是:由于意外地引入了的logback的jar包,破壞了之前的slf4j + slf4j-log4j12 + log4j 的日志架構,使得日志不再通過 log4j 輸出(因而log4j.xml設置的info日志級別也隨之失效),而是通過 logback 輸出,并且logback 默認輸出的是debug級別,因此出現了上述問題。

解決

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

思考

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

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

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

基于 slf4j + slf4j-log4j12 + log4j 的日志架構,是如何工作的?

為了更好的了解其運行原理,我搭建一個簡單的項目。見: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,實現了日志的打印。

<?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包,與日志相關的有:

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,并實現日志打印的呢?

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); // 根據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();
}

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

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

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

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

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

繼續org.slf4j.LoggerFactory#bind()

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

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

小結一下:此方法的實現就是找org.slf4j.impl.StaticLoggerBinder的過程。接下來關注一下具體的過程。

  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 + "]");
        }
        // 打印一個網址,用于解釋多個綁定的情況: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() { // 構建單例
        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;

// 實現了 slf4j-api.jar 中的 ILoggerFactory  接口
public class Log4jLoggerFactory implements ILoggerFactory {

    // key - logger的名字
    // value - org.slf4j.Logger接口的實現類,是個適配器 org.slf4j.impl.Log4jLoggerAdapter
     // 考慮并發的情況,使用了 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,這個典型的適配器模式的實現(有刪減):

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;
    }
    
    // 實現 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());
        }
    }

    ......
}

通過適配器實現了:當我們編程時面向的是org.slf4j.Logger,而實際執行是通過org.apache.log4j.Logger執行打印日志等操作。

總結

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

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

為什么引入了logback就會破壞之前的日志架構?

通過上面的分析,我們知道StaticLoggerBinder是實現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也不會起作用了。這樣,就出現了文章最開始提出的問題中描述的情況
那么,為什么會先加載logback的StaticLoggerBinder呢?這是與ClassLoader的類加載機制雙親委派模型有關的。同一個ClassLoader對于同一個類只會加載一次,所以即使classpath下有多個StaticLoggerBinder.class,也只有1個會被加載,而且在我們當前的環境下恰好加載的就是logback的StaticLoggerBinder
因此,對于slf4j來說,查找到多個StaticLoggerBinder.class并不會影響它的執行,只是打印出警告信息而已。
更多關于此問題的解釋,見:http://www.slf4j.org/codes.html#multiple_bindings

如何避免類似的沖突?

由于slf4j-log4j12.jar或logback.jar中都包含org/slf4j/impl/StaticLoggerBinder.class,為了不讓slf4j加載時產生歧義,需要移除這兩者之一,使slf4j只與1個明確的日志實現綁定。
記住:始終要保證,在classpath下,只有1個org/slf4j/impl/StaticLoggerBinder.class。

作為服務提供方,我們提供的jar如果依賴了日志相關的jar,應該依賴哪些?

通過上面的分析,如作為服務提供方,依賴的日志jar包,只包括slf4j-api.jar即可。多余的jar包(如slf4j-log4j12.jar或logback.jar),可能會引起服務使用方的日志架構出現問題。

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

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

為此,slf4j還提供了橋接機制,將其它日志實現API,通過slf4j,轉換成我們項目中綁定的日志實現。

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

另外,一個干凈的SpringBoot項目,為我們提供了使用slf4j的最佳實踐的范例。根據

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

可以搭建一個干凈的SpringBoot項目。它引入的日志相關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作為項目的日志實現框架

  • 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

),我們也可依此,規范項目的日志框架。

更多

slf4j關于多次綁定的解釋:

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

slf4j橋接遺留的日志API:

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

slf4j的用戶手冊:

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

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