1. 程式人生 > >java 日誌體系(四)log4j 源碼分析

java 日誌體系(四)log4j 源碼分析

setlength const hiera gets exce 長時間 etl 時間 throw

java 日誌體系(四)log4j 源碼分析

logback、log4j2、jul 都是在 log4j 的基礎上擴展的,其實現的邏輯都差不多,下面以 log4j 為例剖析一下日誌框架的基本組件。

一、總體架構

log4j 使用如下:

@Test
public void test() {
    Log log = LogFactory.getLog(JclTest.class);
    log.info("jcl log");
}

log.info 時調用的時序圖如下:

技術分享圖片

在 log4j 的配置文件,我們可以看到其三個最重要的組件:

  1. Logger 每個 logger 可以單獨配置
  2. Appender 每個 appender 可以將日誌輸出到它想要的任何地方(文件、數據庫、消息等等)
  3. Layout 日誌格式布局

這三個組件的關系如下:

技術分享圖片

Log4j API(核心)

  • 日誌對象(org.apache.log4j.Logger):供程序員輸出日誌信息
  • 日誌附加器(org.apache.log4j.Appender):把格式化好的日誌信息輸出到指定的地方去
    • ConsoleAppender - 目的地為控制臺的 Appender
    • FileAppender - 目的地為文件的 Appender
    • RollingFileAppender - 目的地為大小受限的文件的 Appender
  • 日誌格式布局(org.apache.log4j.Layout):用來把程序員的 message 格式化成字符串
    • PatternLayout - 用指定的 pattern 格式化 message的 Layout
  • 日誌過濾器(org.apache.log4j.spi.Filter)
  • 日誌事件(org.apache.log4j.LoggingEvent)
  • 日誌級別(org.apache.log4j.Level)
  • 日誌管理器(org.apache.log4j.LogManager)
  • 日誌倉儲(org.apache.log4j.spi.LoggerRepository)
  • 日誌配置器(org.apache.log4j.spi.Configurator)
  • 日誌診斷上下文(org.apache.log4j.NDC、org.apache.log4j.MDC)

二、日誌管理器(org.apache.log4j.LogManager)

主要職責:

  • 初始化默認 log4j 配置
  • 維護日誌倉儲(org.apache.log4j.spi.LoggerRepository)
  • 獲取日誌對象(org.apache.log4j.Logger)

技術分享圖片

2.1 初始化默認 log4j 配置

LogManager 的靜態代碼塊加載配置文件。

static {
    // 1. 初始化默認的日誌倉庫 Hierarchy(實現了 LoggerRepository 接口) 
    //    DefaultRepositorySelector#getLoggerRepository 簡單的封裝了 LoggerRepository
    Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
    repositorySelector = new DefaultRepositorySelector(h);

    // 2. DEFAULT_CONFIGURATION_KEY=log4j.configuration 配置文件
    //    CONFIGURATOR_CLASS_KEY=log4j.configuratorClass 配置文件解析器,
    //    分 DOMConfigurator 和 PropertyConfigurator 兩類
    String configurationOptionStr = OptionConverter.getSystemProperty(
            DEFAULT_CONFIGURATION_KEY, null);
    String configuratorClassName = OptionConverter.getSystemProperty(
            CONFIGURATOR_CLASS_KEY, null);

    // 3. 根據配置文件路徑加載資源文件
    URL url = null;
    if (configurationOptionStr == null) {
        url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE);
        if (url == null) {
            url = Loader.getResource(DEFAULT_CONFIGURATION_FILE);
        }
    } else {
        try {
            url = new URL(configurationOptionStr);
        } catch (MalformedURLException ex) {
            // so, resource is not a URL:
            // attempt to get the resource from the class path
            url = Loader.getResource(configurationOptionStr);
        }
    }

    // 4. Configurator 解析配置文件
    if (url != null) {
        try {
            OptionConverter.selectAndConfigure(url, configuratorClassName,
                    LogManager.getLoggerRepository());
        } catch (NoClassDefFoundError e) {
            LogLog.warn("Error during default initialization", e);
        }
    } 
}

2.2 日誌倉儲(org.apache.log4j.spi.LoggerRepository)

主要職責:

  • 管理日誌級別閾值(org.apache.log4j.Level)
  • 管理日誌對象(org.apache.log4j.Logger)

LoggerRepository 的主要方法是 getLogger(name),創建一個日誌對象。

// ht 通過 key/value 的形式保存了所有的 logger,其中 key 為類的全路徑,value 為 logger
// logger 有父子關系,每個 logger 的父節點為前一個包名,如果父節點不存在則一直向上查找,直到 rootLogger
// 如果其父節點不存在,使用 ProvisionNode 先進行占位,ProvisionNode 保存有其全部的子節點
// 即 com.github.binarylei.log4j.Log4jTest1 的父節點為 com.github.binarylei.log4j,直到 rootLogger 為止
Hashtable ht;

public Logger getLogger(String name, LoggerFactory factory) {
    CategoryKey key = new CategoryKey(name);
    Logger logger;

    synchronized (ht) {
        Object o = ht.get(key);
        // 1. 日誌倉庫中沒有創建一個
        if (o == null) {
            logger = factory.makeNewLoggerInstance(name);
            logger.setHierarchy(this);
            ht.put(key, logger);
            updateParents(logger);
            return logger;
        // 2. 存在直接返回
        } else if (o instanceof Logger) {
            return (Logger) o;
        // 3. ProvisionNode 占位用
        } else if (o instanceof ProvisionNode) {
            //System.out.println("("+name+") ht.get(this) returned ProvisionNode");
            logger = factory.makeNewLoggerInstance(name);
            logger.setHierarchy(this);
            ht.put(key, logger);
            // ProvisionNode 中的是子節點元素,logger 為當前的父節點
            updateChildren((ProvisionNode) o, logger);
            updateParents(logger);
            return logger;
        } else {
            // It should be impossible to arrive here
            return null;
        }
    }
}

其中有兩個相對比較重要的方法,updateParents 和 updateChildren

// 輪詢父節點,如果存在則直接指定其父節點
// 如果不存在則創建一個 ProvisionNode 用於占位,並設置 ProvisionNode 的子節點
final private void updateParents(Logger cat) {
    String name = cat.name;
    int length = name.length();
    boolean parentFound = false;

    // if name = "w.x.y.z", loop thourgh "w.x.y", "w.x" and "w", but not "w.x.y.z"
    // 輪詢父節點
    for (int i = name.lastIndexOf('.', length - 1); i >= 0;
         i = name.lastIndexOf('.', i - 1)) {
        String substr = name.substring(0, i);
        CategoryKey key = new CategoryKey(substr); // simple constructor
        Object o = ht.get(key);
        // 1. 不存在父節點,創建一個 ProvisionNode 用於占位,設置其子節點為 cat
        if (o == null) {
            ProvisionNode pn = new ProvisionNode(cat);
            ht.put(key, pn);
        // 2. 存在父節點則指定當前 logger 的父節點
        } else if (o instanceof Category) {
            parentFound = true;
            cat.parent = (Category) o;
            break; // no need to update the ancestors of the closest ancestor
        // 3. 如果是 ProvisionNode 直接添加其子節點
        } else if (o instanceof ) {
            ((ProvisionNode) o).addElement(cat);
        } else {
            Exception e = new IllegalStateException("unexpected object type " +
                    o.getClass() + " in ht.");
            e.printStackTrace();
        }
    }
    // If we could not find any existing parents, then link with root.
    if (!parentFound)
        cat.parent = root;
}

// ProvisionNode 保存有當前 logger 的所有子節點
// 創建 logger 時如果找不到父節點則默認為 root,即 l.parent.name=root
// 如果 l.parent 已經是正確的父節點則忽略,否則就需要更新其父節點
final private void updateChildren(ProvisionNode pn, Logger logger) {
    final int last = pn.size();

    for (int i = 0; i < last; i++) {
        Logger l = (Logger) pn.elementAt(i);
        if (!l.parent.name.startsWith(logger.name)) {
            logger.parent = l.parent;
            l.parent = logger;
        }
    }
}

三、日誌對象(org.apache.log4j.Logger)

Logger 繼承自 org.apache.log4j.Priority。Logger 日誌級別: OFF、FATAL、ERROR、INFO、DEBUG、TRACE、ALL。

Logger 最終要的方法是輸出日誌,持有 Appender 才能輸出日誌。

3.1 Logger 管理 Appender

AppenderAttachableImpl 用來管理所有的 Appender,對 logger 上的所有 Appender 進行增刪改查,當前還一個最重要的方法 appendLoopOnAppenders 用於輸出日誌。

AppenderAttachableImpl aai;
public synchronized void addAppender(Appender newAppender) {
    if (aai == null) {
        aai = new AppenderAttachableImpl();
    }
    aai.addAppender(newAppender);
    repository.fireAddAppenderEvent(this, newAppender);
}

3.2 Logger 日誌輸出

public void info(Object message) {
    if (repository.isDisabled(Level.INFO_INT))
        return;
    if (Level.INFO.isGreaterOrEqual(this.getEffectiveLevel()))
        forcedLog(FQCN, Level.INFO, message, null);
}
protected void forcedLog(String fqcn, Priority level, Object message, Throwable t) {
    callAppenders(new LoggingEvent(fqcn, this, level, message, t));
}

callAppenders 最終調用 appender.doAppend(event) 進行日誌輸出。

public void callAppenders(LoggingEvent event) {
    int writes = 0;

    for (Category c = this; c != null; c = c.parent) {
        // Protected against simultaneous call to addAppender, removeAppender,...
        synchronized (c) {
            // 1. 日誌輸出
            if (c.aai != null) {
                writes += c.aai.appendLoopOnAppenders(event);
            }
            // 2. 如果 logger.additive=false 則不會將日誌向上傳遞給父節點 logger
            //    也就是說 additive=false 時日誌不會重復輸出,默認為 true
            //    類似 spring 子容器的事件傳遞給父容器
            if (!c.additive) {
                break;
            }
        }
    }
    // 沒有日誌輸出
    if (writes == 0) {
        repository.emitNoAppenderWarning(this);
    }
}

// AppenderAttachableImpl#appendLoopOnAppenders 用於日誌輸出
public int appendLoopOnAppenders(LoggingEvent event) {
    int size = 0;
    Appender appender;

    if (appenderList != null) {
        size = appenderList.size();
        for (int i = 0; i < size; i++) {
            appender = (Appender) appenderList.elementAt(i);
            // 真正輸出日誌
            appender.doAppend(event);
        }
    }
    return size;
}

3.3 日誌事件(org.apache.log4j.LoggingEvent)

日誌事件是用於承載日誌信息的對象,其中包括:日誌名稱、日誌內容、日誌級別、異常信息(可選)、當前線程名稱、時間戳、嵌套診斷上下文(NDC)、映射診斷上下文(MDC)。

四、日誌附加器(org.apache.log4j.Appender)

日誌附加器是日誌事件(org.apache.log4j.LoggingEvent)具體輸出的介質,如:控制臺、文件系統、網絡套接字等。

日誌附加器(org.apache.log4j.Appender)關聯零個或多個日誌過濾器(org.apache.log4j.Filter),這些過濾器形成過濾鏈。

主要職責:

  • 附加日誌事件(org.apache.log4j.LoggingEvent)
  • 關聯日誌布局(org.apache.log4j.Layout)
  • 關聯日誌過濾器(org.apache.log4j.Filter)
  • 關聯錯誤處理器(org.apache.log4j.spi.ErrorHandler)

相關組件的關系如下,Append 持有 Layout、Filter、ErrorHandler。

技術分享圖片

4.1 Appender 主要流程

技術分享圖片

註意 logger#info 調用 doAppend 時加 synchronized 鎖了,所以是線程安全的,但了同時造成多線程時效率低下。所以才有了後來的 log4j2 和 logback 的出現。

public synchronized void doAppend(LoggingEvent event) {
    // 1. 日誌級別判斷
    if (!isAsSevereAsThreshold(event.getLevel())) {
        return;
    }

    // 2. Filter 過濾
    Filter f = this.headFilter;
    FILTER_LOOP:
    while (f != null) {
        switch (f.decide(event)) {
            // 1. 日誌事件跳過日誌附加器的執行
            case Filter.DENY:
                return;
            // 2. 日誌附加器立即執行日誌事件
            case Filter.ACCEPT:
                break FILTER_LOOP;
            // 3. 跳過當前過濾器,讓下一個過濾器決策
            case Filter.NEUTRAL:
                f = f.getNext();
        }
    }
    // 3. 子類實現,日誌輸出
    this.append(event);
}

doAppend 做日誌過濾,是否進行日誌輸出,真實的日誌輸出則直接委托給了 append 方法。append -> subAppend -> qw.write,QuietWriter 增加了對日誌輸出錯誤時的 ErrorHandler 處理。

public void append(LoggingEvent event) {
    subAppend(event);
}
protected void subAppend(LoggingEvent event) {
    this.qw.write(this.layout.format(event));

    if (layout.ignoresThrowable()) {
        String[] s = event.getThrowableStrRep();
        if (s != null) {
            int len = s.length;
            for (int i = 0; i < len; i++) {
                this.qw.write(s[i]);
                this.qw.write(Layout.LINE_SEP);
            }
        }
    }

    if (shouldFlush(event)) {
        this.qw.flush();
    }
}

4.2 日誌過濾器(org.apache.log4j.spi.Filter)

日誌過濾器用於決策當前日誌事件(org.apache.log4j.spi.LoggingEvent)是否需要在執行所關聯的日誌附加器(org.apache.log4j.Appender)中執行。

決策結果有三種:

  • DENY:日誌事件跳過日誌附加器的執行
  • ACCEPT:日誌附加器立即執行日誌事件
  • NEUTRAL:跳過當前過濾器,讓下一個過濾器決策
public void addFilter(Filter newFilter) {
    if (headFilter == null) {
        headFilter = tailFilter = newFilter;
    } else {
        tailFilter.setNext(newFilter);
        tailFilter = newFilter;
    }
}

4.3 Appender 類繼承關系

技術分享圖片

  • ConsoleAppender - 目的地為控制臺的 Appender
  • FileAppender - 目的地為文件的 Appender
  • RollingFileAppender - 目的地為大小受限的文件的 Appender

WriterAppender 不關心日誌到底寫到那個流中,子類調用 createWriter 來創建一個具體的 Writer,這個 Writer 最終會被 QuietWriter 進行包裝。

// WriterAppender#createWriter
protected OutputStreamWriter createWriter(OutputStream os) {
    OutputStreamWriter retval = null;

    String enc = getEncoding();
    if (enc != null) {
        try {
            retval = new OutputStreamWriter(os, enc);
        } catch (IOException e) {
        }
    }
    if (retval == null) {
        retval = new OutputStreamWriter(os);
    }
    return retval;
}

4.3.1 FileAppender

FileAppender 通過 setFile 方法創建一個 QuietWriter 進行文件定入。

public synchronized void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize)
            throws IOException {
    if (bufferedIO) {
        setImmediateFlush(false);
    }

    reset();
    FileOutputStream ostream = null;
    try {
        ostream = new FileOutputStream(fileName, append);
    } catch (FileNotFoundException ex) {
        ...
    }
    Writer fw = createWriter(ostream);
    if (bufferedIO) {
        fw = new BufferedWriter(fw, bufferSize);
    }
    this.setQWForFiles(fw);
    this.fileName = fileName;
    this.fileAppend = append;
    this.bufferedIO = bufferedIO;
    this.bufferSize = bufferSize;
    writeHeader();
    LogLog.debug("setFile ended");
}

4.3.2 RollingFileAppender 文件大小滾動

RollingFileAppender 根據文件大小進行滾動,有一個重要的屬性 maxFileSize 控制文件大小。RollingFileAppender#subAppend 每次寫日誌時都會判斷是否達到回滾的條件。

protected void subAppend(LoggingEvent event) {
    super.subAppend(event);
    if (fileName != null && qw != null) {
        long size = ((CountingQuietWriter) qw).getCount();
        if (size >= maxFileSize && size >= nextRollover) {
            // 滾動生成新的日誌文件
            rollOver();
        }
    }
}

4.3.3 DailyRollingFileAppender 時間滾動

DailyRollingFileAppender(根據時間滾動) 和 RollingFileAppender(根據文件大小滾動) 差不多,只是回滾的條件不一樣吧了。DailyRollingFileAppender 有一個重要的屬性 datePattern = "‘.‘yyyy-MM-dd" 用於控制多長時間滾動一次,具體配制規則見類註釋。

protected void subAppend(LoggingEvent event) {
    long n = System.currentTimeMillis();
    if (n >= nextCheck) {
        now.setTime(n);
        // 計算一次滾動的時間
        nextCheck = rc.getNextCheckMillis(now);
        try {
            rollOver();
        } catch (IOException ioe) {
            ...
        }
    }
    super.subAppend(event);
}

五、日誌格式布局(org.apache.log4j.Layout)

日誌格式布局用於格式化日誌事件(org.apache.log4j.spi.LoggingEvent)為可讀性的文本內容。

Layout 最重要的方法是 format,將 LoggingEvent 轉換成可讀性的文本內容。

5.1 SimpleLayout

public String format(LoggingEvent event) {
    sbuf.setLength(0);
    sbuf.append(event.getLevel().toString());
    sbuf.append(" - ");
    sbuf.append(event.getRenderedMessage());
    sbuf.append(LINE_SEP);
    return sbuf.toString();
}

5.2 PatternLayout

PatternLayout 可以自定義 LoggingEvent 輸出格式,如 "%r [%t] %p %c %x - %m%n",初始化時會將 pattern 解析為 PatternConverter,PatternConverter 是一個鏈式結構。PatternLayout 自定義規則詳見 PatternLayout 類註釋。

public final static String DEFAULT_CONVERSION_PATTERN = "%m%n";
private StringBuffer sbuf = new StringBuffer(BUF_SIZE);
private String pattern;
private PatternConverter head;

public PatternLayout(String pattern) {
    this.pattern = pattern;
    head = createPatternParser((pattern == null) ? DEFAULT_CONVERSION_PATTERN :
            pattern).parse();
}
protected PatternParser createPatternParser(String pattern) {
    return new PatternParser(pattern);
}

LoggingEvent 格式化時調用 PatternConverter#format 方法,PatternConverter 具體格式化的實現以後有時間再看一下。

public String format(LoggingEvent event) {
    // Reset working stringbuffer
    if (sbuf.capacity() > MAX_CAPACITY) {
        sbuf = new StringBuffer(BUF_SIZE);
    } else {
        sbuf.setLength(0);
    }

    PatternConverter c = head;
    while (c != null) {
        c.format(sbuf, event);
        c = c.next;
    }
    return sbuf.toString();
}

六、日誌配置器(org.apache.log4j.spi.Configurator)

日誌配置器提供外部配置文件配置 log4j 行為的 API,log4j 內建了兩種實現:

  • Properties 文件方式(org.apache.log4j.PropertyConfigurator)
  • XML 文件方式(org.apache.log4j.xml.DOMConfigurator)

每天用心記錄一點點。內容也許不重要,但習慣很重要!

java 日誌體系(四)log4j 源碼分析