1. 程式人生 > >讀logback原始碼系列文章(四)——記錄日誌

讀logback原始碼系列文章(四)——記錄日誌

今天晚上本來想來寫一下Logger怎麼記錄日誌,以及Appender元件。不過9點才從丈母孃家回來,又被幾個兄弟喊去喝酒,結果回來晚了,所以時間就只夠寫一篇Logger類的原始碼分析了。Appender找時間再寫

上篇部落格介紹了LoggerContext怎麼生成Logger,Logger是logback的核心類,也是所有日誌框架的核心類。這篇部落格詳細介紹一下Logger的各欄位和方法,重點介紹Logger類是怎樣記錄日誌的

老規矩,首先看圖:
576c3b42-cb9e-3bfb-8b85-1c72650e1485.jpg

Logger類實現了slf4j框架定義的Logger介面,然後這個類和LoggerContext是互相引用的(因為Logger需要依賴LoggerContext的TurboFilter等元件)。並且Logger實現了AppenderAttachable介面,它實現該介面的方式,是持有AppenderAttachableImpl類,然後委託該類來實現AppenderAttachable介面定義的方法,這裡用到了代理模式,是一個比較精巧的設計

看到了Logger類的全景圖,接下來我們逐一介紹Logger類中的各個欄位,最後重點介紹Logger類是怎樣記錄日誌的

首先看看Logger的欄位

static int instanceCount = 0;

  /**
   * The name of this logger
   */
  private String name;

  // The assigned levelInt of this logger. Can be null.
  private Level level;

  // The effective levelInt is the assigned levelInt and if null, a levelInt is
  // inherited form a parent.
  private int effectiveLevelInt;

  /**
   * The parent of this category. All categories have at least one ancestor
   * which is the root category.
   */
  private Logger parent;

  /**
   * The children of this logger. A logger may have zero or more children.
   */
  private List<Logger> childrenList;

  /**
   * It is assumed that once the 'aai' variable is set to a non-null value, it
   * will never be reset to null. it is further assumed that only place where
   * the 'aai'ariable is set is within the addAppender method. This method is
   * synchronized on 'this' (Logger) protecting against simultaneous
   * re-configuration of this logger (a very unlikely scenario).
   * 
   * <p>
   * It is further assumed that the AppenderAttachableImpl is responsible for
   * its internal synchronization and thread safety. Thus, we can get away with
   * *not* synchronizing on the 'aai' (check null/ read) because
   * <p>
   * 1) the 'aai' variable is immutable once set to non-null
   * <p>
   * 2) 'aai' is getAndSet only within addAppender which is synchronized
   * <p>
   * 3) all the other methods check whether 'aai' is null
   * <p>
   * 4) AppenderAttachableImpl is thread safe
   */
  private transient AppenderAttachableImpl<ILoggingEvent> aai;
  /**
   * Additivity is set to true by default, that is children inherit the
   * appenders of their ancestors by default. If this variable is set to
   * <code>false</code> then the appenders located in the ancestors of this
   * logger will not be used. However, the children of this logger will inherit
   * its appenders, unless the children have their additivity flag set to
   * <code>false</code> too. See the user manual for more details.
   */
  private boolean additive = true;

  final transient LoggerContext loggerContext;
  // loggerRemoteView cannot be final because it may change as a consequence
  // of changes in LoggerContext
  LoggerRemoteView loggerRemoteView;

1、name是Logger的名稱
2、level是該Logger的分配級別,當配置檔案中沒有配置時,這個分配級別可以為null
3、effectiveLevelInt是該Logger的生效級別,會從父Logger繼承得到
4、parent和childrenList是這個Logger的父Logger和子Logger,體現了logback的Logger層次結構
5、aai上面已經說到了,Logger是委託這個類實現AppenderAttachable介面,也是委託這個類來呼叫Appender元件來實際記錄日誌,所以這個欄位是最關鍵的
6、additive是這個類的Appender疊加性,具體看我的另一篇部落格。該欄位也是在配置檔案中配置的,預設為true
7、loggerRemoteView也是一個VO物件,作用不是很大

介紹完了欄位,可以看到Logger的設計還是相當清晰易懂的, 接下來逐一看看Logger中的方法,getter和setter方法就不廢話了

private final boolean isRootLogger() {
    // only the root logger has a null parent
    return parent == null;
  }

這個方法用來判斷一個Logger是否是根Logger

Logger getChildByName(final String childName) {
    if (childrenList == null) {
      return null;
    } else {
      int len = this.childrenList.size();
      for (int i = 0; i < len; i++) {
        final Logger childLogger_i = (Logger) childrenList.get(i);
        final String childName_i = childLogger_i.getName();

        if (childName.equals(childName_i)) {
          return childLogger_i;
        }
      }
      // no child found
      return null;
    }
  }

這個方法用來得到子Logger,是在LoggerContext的getLogger()方法裡呼叫的,在上一篇部落格裡已經介紹過了

public synchronized void setLevel(Level newLevel) {
    if (level == newLevel) {
      // nothing to do;
      return;
    }
    if (newLevel == null && isRootLogger()) {
      throw new IllegalArgumentException(
          "The level of the root logger cannot be set to null");
    }

    level = newLevel;
    if (newLevel == null) {
      effectiveLevelInt = parent.effectiveLevelInt;
    } else {
      effectiveLevelInt = newLevel.levelInt;
    }

    if (childrenList != null) {
      int len = childrenList.size();
      for (int i = 0; i < len; i++) {
        Logger child = (Logger) childrenList.get(i);
        // tell child to handle parent levelInt change
        child.handleParentLevelChange(effectiveLevelInt);
      }
    }
    // inform listeners
    loggerContext.fireOnLevelChange(this, newLevel);
  }

  /**
   * This method is invoked by parent logger to let this logger know that the
   * prent's levelInt changed.
   * 
   * @param newParentLevel
   */
  private synchronized void handleParentLevelChange(int newParentLevelInt) {
    // changes in the parent levelInt affect children only if their levelInt is
    // null
    if (level == null) {
      effectiveLevelInt = newParentLevelInt;

      // propagate the parent levelInt change to this logger's children
      if (childrenList != null) {
        int len = childrenList.size();
        for (int i = 0; i < len; i++) {
          Logger child = (Logger) childrenList.get(i);
          child.handleParentLevelChange(newParentLevelInt);
        }
      }
    }
  }

這2個方法是用來改變Logger的生效級別,並且連帶改變子Logger的生效級別

Logger createChildByName(final String childName) {
    int i_index = getSeparatorIndexOf(childName, this.name.length() + 1);
    if (i_index != -1) {
      throw new IllegalArgumentException("For logger [" + this.name
          + "] child name [" + childName
          + " passed as parameter, may not include '.' after index"
          + (this.name.length() + 1));
    }

    if (childrenList == null) {
      childrenList = new ArrayList<Logger>(DEFAULT_CHILD_ARRAY_SIZE);
    }
    Logger childLogger;
    childLogger = new Logger(childName, this, this.loggerContext);
    childrenList.add(childLogger);
    childLogger.effectiveLevelInt = this.effectiveLevelInt;
    return childLogger;
  }

這個方法是用來建立子Logger的,並且會設定Logger的父子關係,也是在LoggerContext的getLogger()方法裡呼叫的

接下來就重點介紹Logger元件怎麼記錄日誌了,slf4j定義了Logger介面記錄日誌的方法是info()、warn()、debug()等,這些方法只是入口,logback是這樣實現這些方法的

public void info(String msg) {
    filterAndLog_0_Or3Plus(FQCN, null, Level.INFO, msg, null, null);
  }

當客戶端程式碼呼叫Logger.info()時,實際上會進入filterAndLog_0_Or3Plus方法,Logger類中還有很多名字很相似的方法,比如filterAndLog_1、filterAndLog_2。據作者自己說,之所以定義這一系列的方法,是為了提高logback的效能

/**
   * The next methods are not merged into one because of the time we gain by not
   * creating a new Object[] with the params. This reduces the cost of not
   * logging by about 20 nanoseconds.
   */

  private final void filterAndLog_0_Or3Plus(final String localFQCN,
      final Marker marker, final Level level, final String msg,
      final Object[] params, final Throwable t) {

    final FilterReply decision = loggerContext
        .getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg,
            params, t);

    if (decision == FilterReply.NEUTRAL) {
      if (effectiveLevelInt > level.levelInt) {
        return;
      }
    } else if (decision == FilterReply.DENY) {
      return;
    }

    buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t);
  }

該方法首先要請求TurboFilter來判斷是否允許記錄這次日誌資訊。TurboFilter是快速篩選的元件,篩選發生在LoggingEvent建立之前,這種設計也是為了提高效能

如果經過過濾,確定要記錄這條日誌資訊,則進入buildLoggingEventAndAppend方法

private void buildLoggingEventAndAppend(final String localFQCN,
      final Marker marker, final Level level, final String msg,
      final Object[] params, final Throwable t) {
    LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
    le.setMarker(marker);
    callAppenders(le);
  }

在這個方法裡,首先建立了LoggingEvent物件,然後呼叫callAppenders()方法,要求該Logger關聯的所有Appenders來記錄日誌

LoggingEvent物件是承載了日誌資訊的類,最後輸出的日誌資訊,就來源於這個事件物件

/**
   * Invoke all the appenders of this logger.
   * 
   * @param event
   *          The event to log
   */
  public void callAppenders(ILoggingEvent event) {
    int writes = 0;
    for (Logger l = this; l != null; l = l.parent) {
      writes += l.appendLoopOnAppenders(event);
      if (!l.additive) {
        break;
      }
    }
    // No appenders in hierarchy
    if (writes == 0) {
      loggerContext.noAppenderDefinedWarning(this);
    }
  }

經過前面的Filter過濾、日誌級別匹配、建立LoggerEvent物件,終於進入了記錄日誌的方法。該方法會呼叫此Logger關聯的所有Appender,而且還會呼叫所有父Logger關聯的Appender,直到遇到父Logger的additive屬性設定為false為止,這也是為什麼如果子Logger和父Logger都關聯了同樣的Appender,則日誌資訊會重複記錄的原因

繼續看下來

private int appendLoopOnAppenders(ILoggingEvent event) {
    if (aai != null) {
      return aai.appendLoopOnAppenders(event);
    } else {
      return 0;
    }
  }

實際上呼叫的AppenderAttachableImpl的appendLoopOnAppenders()方法

/**
   * Call the <code>doAppend</code> method on all attached appenders.
   */
  public int appendLoopOnAppenders(E e) {
    int size = 0;
    r.lock();
    try {
      for (Appender<E> appender : appenderList) {
        appender.doAppend(e);
        size++;
      }
    } finally {
      r.unlock();
    }
    return size;
  }

到這裡,為了記錄一條日誌資訊,長長的呼叫鏈終於告一段落了,通過呼叫Appender的doAppend(LoggingEvent e)方法,委託Appender來最終記錄日誌(其實Appender記錄日誌資訊也是委託其他的類來完成的, 在後面的部落格中再介紹)

但是這裡沒這麼簡單,AppenderAttachableImpl類為了處理併發情況,是用了讀寫鎖的

final private ReadWriteLock rwLock = new ReentrantReadWriteLock();
  private final Lock r = rwLock.readLock();
  private final Lock w = rwLock.writeLock();

總結來說,Logger類中定義的欄位和方法,是出於以下目的:
1、持有LoggerContext,是為了使用TurboFilter來進行快速過濾
2、定義parent和childList,用於實現父子Logger的樹形結構
3、定義createChildByName()、getChildByName()方法,是供LoggerContext建立Logger
4、定義level、effectiveLevelInt,是為了判定日誌級別是否足夠
5、最後,filterAndLog()、buildLoggingEventAndAppend()、callAppenders()、appendLoopOnAppenders()方法,是Logger類的核心方法,一步步地委託AppenderAttachableImpl類來實際記錄日誌

下一篇部落格,準備介紹Appender元件怎麼記錄日誌
  • 576c3b42-cb9e-3bfb-8b85-1c72650e1485-thumb.jpg
  • 大小: 141.6 KB

相關推薦

logback原始碼系列文章——記錄日誌

今天晚上本來想來寫一下Logger怎麼記錄日誌,以及Appender元件。不過9點才從丈母孃家回來,又被幾個兄弟喊去喝酒,結果回來晚了,所以時間就只夠寫一篇Logger類的原始碼分析了。Appender找時間再寫 上篇部落格介紹了LoggerContext怎麼生成Logger

logback原始碼系列文章——記錄日誌的實際工作類Encoder

本系列的部落格從logback怎麼對接slf4j開始,逐步介紹了LoggerContext、Logger、ContextInitializer、Appender、Action等核心元件。跟讀logback的原始碼到這個程度,雖然不能說精通,不過至少日常的配置,和簡單的自定義擴

TiDB Binlog 原始碼閱讀系列文章Pump server 介紹

作者: satoru 在 上篇文章 中,我們介紹了 TiDB 如何通過 Pump client 將 binlog 發往 Pump,

openstack系列文章

cnblogs 調度器 5.5 min 代碼位置 虛機 inux latest 階段 學習 openstack 的系列文章 - Nova Nova 基本概念 Nova 架構 openstack Log Nova 組件介紹 Nova 操作介紹 1. Nova 基本概念

Git系列文章:常見異常問題

1、GitHub提交的時顯示Updates were rejected because the remote contains work that you do  git push -u origin master 每次建立新的倉庫,提交的時總會出現這樣的錯誤。Updates

【NLP】揭祕馬爾可夫模型神祕面紗系列文章

作者:白寧超 2016年7月12日14:08:28 摘要:最早接觸馬爾可夫模型的定義源於吳軍先生《數學之美》一書,起初覺得深奧難懂且無什麼用場。直到學習自然語言處理時,才真正使用到隱馬爾可夫模型,並體會到此模型的妙用之處。馬爾可夫模型在處理序列分類時具體強大的功能,諸如解決:詞類標註、語音識別、句

TiDB 原始碼閱讀系列文章初識 TiDB 原始碼

本文為 TiDB 原始碼閱讀系列文章的第二篇,第一篇文章介紹了 TiDB 整體的架構,知道 TiDB 有哪些模組,分別是做什麼的,從哪裡入手比較好,哪些可以忽略,哪些需要仔細閱讀。 這篇文章是一篇入門文件,難度係數比較低,其中部分內容可能大家在其他渠道已經看過

TiKV 原始碼解析系列文章Prometheus

開發十年,就只剩下這套架構體系了! >>>   

TiKV 原始碼解析系列文章gRPC Server 的初始化和啟動流程

作者:屈鵬 本篇 TiKV 原始碼解析將為大家介紹 TiKV 的另一週邊元件—— grpc-rs。grpc-rs 是 PingCA

DM 原始碼閱讀系列文章定製化資料同步功能的實現

作者:王相 本文為 DM 原始碼閱讀系列文章的第七篇,在 上篇文章 中我們介紹了 relay log 的實現,主要包括 relay

DM 原始碼閱讀系列文章Online Schema Change 同步支援

作者:lan 本文為 DM 原始碼閱讀系列文章的第八篇,上篇文章 對 DM 中的定製化資料同步功能進行詳細的講解,包括庫表路由(T

DM 原始碼閱讀系列文章shard DDL 與 checkpoint 機制的實現

作者:張學程 本文為 DM 原始碼閱讀系列文章的第九篇,在 上篇文章 中我們詳細介紹了 DM 對 online schema ch

DM 原始碼閱讀系列文章測試框架的實現

作者:楊非 本文為 DM 原始碼閱讀系列文章的第十篇,之前的文章已經詳細介紹過 DM 資料同步各元件的實現原理和程式碼解析,相信大

Java系列文章

java 學習JVMJVM系列:類裝載器的體系結構 JVM系列:Class文件檢驗器JVM系列:安全管理器JVM系列:策略文件Java垃圾回收機制深入剖析Classloader(一)--類的主動使用與被動使用深入剖析Classloader(二)-根類加載器,擴展類加載器與系統類加載器深入理解JVM—JVM內存

JXLS 2.4.0系列教程——多sheet是怎麽做到的

while director write 教程 == 模板 phy sheet ack 註:本文代碼在第一篇文章基礎上修改而成,請務必先閱讀第一篇文章。 http://www.cnblogs.com/foxlee1024/p/7616987.html 本文也不會過多的講解模

JXLS 2.4.0系列教程——拾遺 如何做頁面小計

進行 line http spa shee shel nes 默認 閱讀   註:閱讀本文前,請先閱讀第四篇文章。   http://www.cnblogs.com/foxlee1024/p/7619845.html   前面寫了第四篇教程,發現有些東西忘了講了,這裏補

回顧2017系列:永不過時的設計資源

如果你是一個留心者,你會發現近幾年的設計潮流和趨勢隨著科技的革新在不斷的更替和進步。網絡上的設計資源,教學視頻也愈加的豐富和多樣,為眾多設計行業的後來者提供了巨大的便利。設計們也樂於分享自己的設計經驗和技巧為初入行的菜鳥們提供幫助和指導。今天的設計行業也不再那麽神秘。 2017年是設計行業發

【WCF系列WCF客戶端怎麽消費服務

class fig 完全 文件 自動 客戶 回收 ins 必須 WCF客戶端怎麽消費服務 獲取服務綁定協議、綁定和地址:實現方式 SvcUtil方式:SvcUtil.exe是一個命令行工具,位於:C:\Program Files (x86)\Microsoft SDKs

Android框架原始碼解析之Picasso

這次要分析的原始碼是 Picasso 2.5.2 ,四年前的版本,用eclipse寫的,但不影響這次我們對其原始碼的分析 地址:https://github.com/square/picasso/tree/picasso-parent-2.5.2 Picasso的簡單使用

Tensorflow系列專題:神經網路篇之前饋神經網路綜述

目錄: 神經網路前言 神經網路 感知機模型 多層神經網路 啟用函式 Logistic函式 Tanh函式 ReLu函式 損失函式和輸出單元 損失函