JavaWeb 開發日誌管理詳解(內含Slf4j底層實現原理 + log4j + logback框架介紹)

日誌管理系統的使用背景

我們在日常軟件開發中,避免不了使用日誌管理系統,它是爲我們展示系統運行狀況的重要手段,當我們的系統在上線運行後,每個模塊代碼的執行,或者產生的結果乃至突發的錯誤,我們都可以通過日誌管理系統記錄下來,在我們需要某些數據或者處理突發錯誤時能夠快速定位問題代碼的位置或者原因時,會非常的便利和高效。如果我們沒有采取任何的代碼執行記錄,那一旦我們的系統出現問題,或者業務邏輯出現問題,我們在維護起來會非常的麻煩,因爲我們無法定位問題所在的地方,也無法得知系統運行中數據的狀態,那隻能將所有代碼一點一點的過濾,來確定錯誤所在位置,對於小的軟件系統還勉強說得過去,稍微大一點的系統,這完全就是在大海撈針。所以,在我們日常的軟件系統開發過程中,利用日誌管理系統來即時記錄代碼運行結果或者異常處理時的位置,對於我們後期維護是非常必要的。

日誌管理系統是什麼?

其實“日誌管理系統”是我們業內的專有名詞,它的本質其實就是我們在業務代碼中,穿插一些特殊的代碼,這些特殊的代碼用來記錄一些我們想記錄的內容。比如:當我們還是小白的時候,很多時候我們在排錯的時候,喜歡使用System.out.println() 來打印一些變量的值,用於我們的檢查,或者是打印一些我們制定的字符串內容,用來表示是否進入到了我們指定的方法裏。其實,日誌管理系統,做的就是這種事情,因爲我們在實際開發和項目運行後,是不會用打印語句在控制檯窗口上查看我們打印內容的,這樣會產生非常多沒用的代碼,僅僅是用來方便我們開發人員排錯或者監管系統運行,所以,我們不能用打印語句這種Low的辦法,取而代之的,就是咱們說的日誌管理系統。所謂管理系統,其實就是在書寫核心業務邏輯代碼的同時,再寫一套關於打印出我們指定的內容的系統,但是,這種系統,其實開發一次就足夠了,因爲功能應該都是一樣的,比如:什麼時候打印,需要打印什麼內容,把內容打印到哪裏去?(是控制檯上,還是可以單獨的用IO輸出成磁盤文件已達到持久化)那有了這些基本的內容,我們就可以自己去開發一套這樣的系統了,然後在我們的每個項目中都可以引入使用。
那既然說到這裏了,那麼業內常說的log4j、logback、Apache common logging 其實都是日誌框架,至於做了什麼事,現在大家應該很清楚了吧,這種日誌框架,其實就是前輩們自己寫的一套作日誌記錄的系統代碼,然後把它進行了封裝,可以給現在的我們直接配置使用,就不用我們自己再去花費物力財力腦力開發了。那有同學可能會問,SUN公司官方jdk有沒有直接提供一套這種日誌框架呢,其實是有的,叫JDKLog,JDKLog 的優點是使用非常簡單,直接在 JDK 中就可以使用。但 JDKLog 功能比較太過於簡單,不支持佔位符顯示,拓展性比較差,所以現在用的人也很少。比如把JDKLog、log4j、logback同時對比的話,JDKLog就是小刀,Log4j是大炮,Logback是原子彈,這種形象的對比,相信不用多說大家就知道他們的分量了,那麼具體的差別,請繼續往下看。

Java Web中的各種日誌管理系統?

1. JDK原生日誌API
  • JDK中內置了原生的日誌打印的API:java.util.logging.Logger,這個API不依賴於任何的第三方框架,是JDK內置的
  • 這個Logger的級別: SEVERE → WARNING → INFO → CONFIG → FINE → FINER → FINESET
  • 用法如下:
private Logger logger = Logger.getLogger("<class-name>");
logger.severe("<severe-msg>");
logger.warning("<warning-msg>");
logger.info("<info-msg>");
logger.config("<config-msg>");
logger.fine("<fine-msg>");
logger.finer("<finer-msg>");
logger.finest("<finest-msg>");
  • JDK日誌配置
    默認情況下,日誌的輸出都是在控制檯直接輸出的,並沒有輸出到文件中,這是因爲在JRE默認配置中,只配置了一個ConsoleHandler,這個配置文件位於/lib/logging.properties,注意一般情況下我在安裝JDK的時候是不會安裝公共JRE的,因爲JDK中就已經安裝過了,不過最新的JDK貌似已經把二者分開了,總之,根據自己的情況找到JRE的家目錄,即可找到該文件;
    在logging.properties這個文件中有個handlers,其值只有一個handlers= java.util.logging.ConsoleHandler,如果想要同時輸出到文件中, 只需要在後面追加java.util.logging.FileHandler,多個handler之間使用英文都好隔開即可;
    在增加了FileHanlder之後,那麼FileHanlder的各種配置就生效了,就可以根據需要進行配置了,如:
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
  1. FileHandler.pattern:用來指定生成的日誌文件的路徑名稱;/:本地路徑名分隔符;%t:系統臨時目錄;%h:“user.home” 系統屬性的值;%g:區分循環日誌的生成號;%u:解決衝突的惟一號碼;%%:轉換爲單個百分數符號"%" ;
  2. FileHandler.limit:用來限制文件的大小,以字節爲單位,0表示沒有限制;
  3. FileHandler.count:指定有多少輸出文件參與循環(默認爲1);
  4. FileHandler.formatter:指定要使用的 Formatter 類的名稱(默認爲java.util.logging.XMLFormatter)。 另外一個是:java.util.logging.SimpleFormatter。XMLFormatter是以xml樣式輸出,SimpleFormatter是以普通樣式輸出。
  5. FileHandler.append:指定是否應該將 FileHandler 追加到任何現有文件上(默認爲false);
  6. SimpleFormatter.format:用來自定義SimpleFormatter的輸出樣式;如:%4s:s: %5s [%1$tc]%n,即輸出樣式爲:: [<date/time>]
  • 日誌打印級別的配置:
  1. 全局輸出級別設置
[java.util.logging.ConsoleHandler/FileHandler].level=<level-name>
  1. 具體包名的輸出級別設置
<package-name>.level=<level-name>
2. Apache common logging
  • Tomcat是Apache的一個開源項目,Apache旗下還有一個開源項目,名爲Apache common logging,而Tomcat中的日誌框架juli正是這個開源框架的一個易名項目,究其根源來說,Apache common logging是對JDK打印API的修改封裝;

  • 爲了不影響JDK中的Logger的正常使用,Tomcat中增加了自定義的Log配置,這個配置存在於Tomcat的腳本catalina.bat和catalina.sh中,如下:

LOGGING_CONFIG="-Djava.util.logging.config.file=$CATALINA_BASE/conf/logging.properties"
LOGGING_MANAGER="-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager"
  1. 這兩個參數配置給java虛擬機配置了兩個東東, 一個是日誌的配置文件還有一個日誌管理器;
  • 在這個配置文件中,我們發現默認情況下,Tomcat支持文件和控制檯兩種輸出方式;還有就是Tomcat的handler都帶有一個數字前綴,自然就想到如果使用系統默認的類加載器的話,肯定會出錯,Tomcat使用的是自定義的ClassLoader來解析這個數字前綴;而增加數字前綴的目的就是爲了使得一個類模板生成多個實例,每個實例都管理一種類型的日誌,如:
  1. 前綴1對應的日誌:文件名以catalina.開頭
  2. 前綴2對應的日誌:文件名以localhost.開頭
  3. 前綴3對應的日誌:文件名以manager.開頭
  4. 前綴4對應的日誌:文件名以host-manager.開頭
  • Tomcat與JDK日誌的配置
  1. 有一點不同,JRE的logging.properties文件中使用handlers配置輸出途徑,而在Tomcat中使用.handlers配置,使用handlers配置上面4中不同的文件日誌
  2. Tomcat的詳細配置可查看:http://tomcat.apache.org/tomcat-8.0-doc/config [/valve.html]
3. Logback的API(Log4j)

提起這個日誌框架,就不得不提起另外另個東東:slf4j和log4j,這三個框架什麼關係呢?網上找到一張圖:
在這裏插入圖片描述

  • slf4j的全稱是:The Simple Logging Facade for(4) Java,即slf4j是一個門面,而log4j和logback則是這些接口的實現;log4j和logback是由同一個人完成的,前者是apache實現的,logback是爲了替代log4j,logback可以說是slf4j的原生實現,目前除了老的項目,新項目的日誌基本上都是使用logback;
  1. logback是直接實現了slf4j的接口,不消耗內存和計算開銷的。slf4j的api在調用log4j時需要一個適配層;
  2. log4j和logback可以單獨的使用,也可以綁定slf4j一起使用:單獨使用的時候,分別調用框架自己的方法來輸出日誌信息;綁定slf4j一起使用,調用slf4j的api來輸入日誌信息,具體使用與底層日誌框架無關,但是需要底層框架的配置文件;
  • 其實說到這裏就能發現這個門面扮演什麼角色,其實就是爲了解決衆口難調的問題,因爲項目中我們用到很多的框架,如果每個框架都有自己的日誌輸出框架,那麼項目中的日誌維護便成了一個大問題,爲了解決這個大問題,log4j和logback框架橫空出世:我們項目中的所有框架的日誌輸出都交給slf4j,然後統一對消息進行處理;

slf4j 又是什麼?

剛纔看完了上面幾種日誌框架的介紹,想必同學們也對日誌框架多少了解了一些,不光知道了框架有多種多樣,同時配置起來也是各不相同,甚至功能也相差甚遠。但是,可能有的同學在讀到上面一段內容時,遇到了Slf4j這個名詞,對它並不熟悉,其實它是幹啥的呢?我們爲什麼要用它呢?
slf4j是門面模式的典型應用,因此在講slf4j前,我們先簡單回顧一下門面模式,
門面模式,其核心爲外部與一個子系統的通信必須通過一個統一的外觀對象進行,使得子系統更易於使用。用一張圖來表示門面模式的結構爲:
門面模式介紹
門面模式的核心爲Facade即門面對象,門面對象核心爲幾個點:
• 知道所有子角色的功能和責任
• 將客戶端發來的請求委派到子系統中,沒有實際業務邏輯
• 不參與子系統內業務邏輯的實現
大致上來看,對門面模式的回顧到這裏就可以了,開始接下來對SLF4J的學習。

我們爲什麼要使用slf4j?
舉個例子,比如:我們自己的系統中使用了logback這個日誌系統。
同時,我們的系統又使用了第三方的jar包——A.jar,而A.jar中使用的日誌系統爲log4j。
甚至,我們的系統又使用了第三方的jar包——B.jar,而B.jar中使用的日誌系統爲slf4j-simple
那這樣,我們的系統就不得不同時支持並維護logback、log4j、slf4j-simple三種日誌框架,非常的不方便。
那當這種問題出現的時候,前輩們就想辦法開始解決這種問題,解決這種問題無非就是,我們開發人員就不要去自己維護這幾種不同風格的框架了,我們再寫一套代碼,專門來處理這種情況——多個日誌框架同時存在需要維護。具體做什麼呢?就是我們只需要告訴這套代碼,我想用哪個牌子的日誌管理系統,然後具體調用哪個框架怎麼去記錄、怎麼打印等等這些具體的事情,就交給這套代碼去做了,我們開發者只需要關注,用啥框架、需要記錄啥內容即可。

走進Slf4j的世界

那解決這個問題的方式就是引入一個適配層,由適配層決定使用哪一種日誌系統,而調用端只需要做的事情就是打印日誌而不需要關心如何打印日誌,slf4j或者commons-logging就是這種適配層,接下來我們就研究一下slf4j。

從上面的描述,我們必須清楚地知道一點:slf4j只是一個日誌標準,並不是日誌系統的具體實現。具體的實現還是那些log4j、logback等日誌框架。

理解這句話非常重要,slf4j只做兩件事情:
• 提供日誌接口
• 提供獲取具體日誌對象的方法

slf4j-simple、logback都是slf4j的具體實現,log4j並不直接實現slf4j,但是有專門的一層橋接slf4j-log4j12來實現slf4j。

爲了更理解slf4j,我們先看例子,再讀源碼,相信大家會對slf4j有更深刻的認識。

slf4j應用舉例

上面講了,slf4j的直接/間接實現有slf4j-simple、logback、slf4j-log4j12,我們先定義一個pom.xml,引入相關jar包:

<dependencies>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.11</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-api</artifactId>
		<version>1.7.25</version>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback</groupId>
		<artifactId>logback-classic</artifactId>
		<version>1.2.3</version>
	</dependency>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-simple</artifactId>
		<version>1.7.25</version>
	</dependency>
	<dependency>
		<groupId>log4j</groupId>
		<artifactId>log4j</artifactId>
		<version>1.2.17</version>
	</dependency>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-log4j12</artifactId>
		<version>1.7.21</version>
	</dependency>
</dependencies>

寫一段簡單的Java代碼:

  @Test
  public void testSlf4j() {
      Logger logger = LoggerFactory.getLogger(Object.class);
      logger.error("123");
  }

接着,我們將pom.xml中引入的依賴,除了junit和slf4j-api之外,全部刪掉。如下:

<dependencies>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.11</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-api</artifactId>
		<version>1.7.25</version>
	</dependency>
</dependencies>

其實這麼做,就是爲了給大家說明一下剛纔強調的內容。即不引入任何slf4j的實現類,運行Test方法,我們看一下控制檯的輸出爲:
在這裏插入圖片描述
看到沒有任何日誌的輸出,這驗證了剛纔說的:slf4j不提供日誌的具體實現,只有slf4j是無法打印日誌的。
接着打開logback-classic的註釋,運行Test方法,我們看一下控制檯的輸出爲:

22:02:14.552 [main] ERROR java.lang.Object - 123

可以看到,我們只要引入了一個slf4j的具體實現類,即可使用該日誌框架輸出日誌。

最後做一個測驗,我們把所有日誌打開,引入logback-classic、slf4j-simple、log4j,運行Test方法,控制檯輸出爲:
在這裏插入圖片描述
和上面的差別是,可以輸出日誌,但是會輸出一些警告日誌,提示我們同時引入了多個slf4j的實現,然後展示了,選擇了其中的哪一個作爲了我們默認使用的日誌系統。

從例子我們可以得出一個重要的結論,即slf4j的作用:只要所有代碼都使用門面對象slf4j,我們就不需要關心其具體實現,最終所有地方使用一種具體實現即可,更換、維護都非常方便。

slf4j實現原理

上面看了slf4j的示例,下面研究一下slf4j的實現,我們只關注重點代碼。

slf4j的用法就是常年不變的一句"Logger logger = LoggerFactory.getLogger(Object.class);",可見這裏就是通過LoggerFactory去拿slf4j提供的一個Logger接口的具體實現而已,LoggerFactory的getLogger的方法實現爲:

 1 public static Logger getLogger(Class<?> clazz) {
 2     Logger logger = getLogger(clazz.getName());
 3     if (DETECT_LOGGER_NAME_MISMATCH) {
 4         Class<?> autoComputedCallingClass = Util.getCallingClass();
 5         if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
 6             Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
 7                             autoComputedCallingClass.getName()));
 8             Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
 9         }
10     }
11     return logger;
12 }

從第2行開始跟代碼,一直跟到LoggerFactory的bind()方法:

1 private final static void bind() {
 2     try {
 3         Set<URL> staticLoggerBinderPathSet = null;
 4         // skip check under android, see also
 5         // http://jira.qos.ch/browse/SLF4J-328
 6         if (!isAndroid()) {
 7             staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
 8             reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
 9         }
10         // the next line does the binding
11         StaticLoggerBinder.getSingleton();
12         INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
13         reportActualBinding(staticLoggerBinderPathSet);
14         fixSubstituteLoggers();
15         replayEvents();
16         // release all resources in SUBST_FACTORY
17         SUBST_FACTORY.clear();
18     } catch (NoClassDefFoundError ncde) {
19         String msg = ncde.getMessage();
20         if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
21             INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
22             Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
23             Util.report("Defaulting to no-operation (NOP) logger implementation");
24             Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
25         } else {
26             failedBinding(ncde);
27             throw ncde;
28         }
29     } catch (java.lang.NoSuchMethodError nsme) {
30         String msg = nsme.getMessage();
31         if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
32             INITIALIZATION_STATE = FAILED_INITIALIZATION;
33             Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
34             Util.report("Your binding is version 1.5.5 or earlier.");
35             Util.report("Upgrade your binding to version 1.6.x.");
36         }
37         throw nsme;
38     } catch (Exception e) {
39         failedBinding(e);
40         throw new IllegalStateException("Unexpected initialization failure", e);
41     }
42 }

這個地方第7行是一個關鍵,看一下代碼:

1 static Set<URL> findPossibleStaticLoggerBinderPathSet() {
 2     // use Set instead of list in order to deal with bug #138
 3     // LinkedHashSet appropriate here because it preserves insertion order
 4     // during iteration
 5     Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
 6     try {
 7         ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
 8         Enumeration<URL> paths;
 9         if (loggerFactoryClassLoader == null) {
10             paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
11         } else {
12             paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
13         }
14         while (paths.hasMoreElements()) {
15             URL path = paths.nextElement();
16             staticLoggerBinderPathSet.add(path);
17         }
18     } catch (IOException ioe) {
19         Util.report("Error getting resources from path", ioe);
20     }
21     return staticLoggerBinderPathSet;
22 }

這個地方重點其實就是第12行的代碼,getLogger的時候會去classpath下找STATIC_LOGGER_BINDER_PATH,STATIC_LOGGER_BINDER_PATH值爲"org/slf4j/impl/StaticLoggerBinder.class",即所有slf4j的實現,在提供的jar包路徑下,一定是有"org/slf4j/impl/StaticLoggerBinder.class"存在的,我們可以看一下:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
我們不能避免在系統中同時引入多個slf4j的實現,所以接收的地方是一個Set。大家應該注意到,上部分在演示同時引入logback、slf4j-simple、log4j的時候會有警告:
在這裏插入圖片描述
這就是因爲有三個"org/slf4j/impl/StaticLoggerBinder.class"存在的原因,此時reportMultipleBindingAmbiguity方法控制檯輸出語句:

1 private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
2     if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
3         Util.report("Class path contains multiple SLF4J bindings.");
4         for (URL path : binderPathSet) {
5             Util.report("Found binding in [" + path + "]");
6         }
7         Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
8     }
9 }

那有同學可能會問,同時存在三個"org/slf4j/impl/StaticLoggerBinder.class"怎麼辦?首先確定的是這不會導致啓動報錯,其次在這種情況下編譯期間,編譯器會選擇其中一個StaticLoggerBinder.class進行綁定,這個地方sfl4j也在reportActualBinding方法中報告了綁定的是哪個日誌框架:

1 private static void reportActualBinding(Set<URL> binderPathSet) {
2     // binderPathSet can be null under Android
3     if (binderPathSet != null && isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
4         Util.report("Actual binding is of type [" + StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr() + "]");
5     }
6 }

對照上面的截圖,看最後一行,確實是"Actual binding is of type…"這句。

最後StaticLoggerBinder就比較簡單了,不同的StaticLoggerBinder其getLoggerFactory實現不同,拿到ILoggerFactory之後調用一下getLogger即拿到了具體的Logger,可以使用Logger進行日誌輸出。

走進Logback的世界

在五年前,大部分公司使用的日誌框架還是log4j,大約從16年中到現在,大部分公司的日誌框架基本都換成了logback,總結一下,logback大約有以下的一些優點:

  • 內核重寫、測試充分、初始化內存加載更小,這一切讓logback性能和log4j相比有諸多倍的提升
  • logback非常自然地直接實現了slf4j,這個嚴格來說算不上優點,只是這樣,再理解slf4j的前提下會很容易理解logback,也同時很容易用其他日誌框架替換logback
  • logback有比較齊全的200多頁的文檔
  • logback當配置文件修改了,支持自動重新加載配置文件,掃描過程快且安全,它並不需要另外創建一個掃描線程
  • 支持自動去除舊的日誌文件,可以控制已經產生日誌文件的最大數量

總而言之,如果大家的項目裏面需要選擇一個日誌框架,那麼我個人非常建議使用logback,這也就是爲什麼在文章最開始,說它的原子彈。

- logback加載

我們簡單分析一下logback加載過程,當我們使用logback-classic.jar時,應用啓動,那麼logback會按照如下順序進行掃描:

  • 在系統配置文件System Properties中尋找是否有logback.configurationFile對應的value
  • 在classpath下尋找是否有logback.groovy(即logback支持groovy與xml兩種配置方式)
  • 在classpath下尋找是否有logback-test.xml
  • 在classpath下尋找是否有logback.xml

以上任何一項找到了,就不進行後續掃描,按照對應的配置進行logback的初始化,具體代碼實現可見ch.qos.logback.classic.util.ContextInitializer類的findURLOfDefaultConfigurationFile方法。

當所有以上四項都找不到的情況下,logback會調用ch.qos.logback.classic.BasicConfigurator的configure方法,構造一個ConsoleAppender用於向控制檯輸出日誌,默認日誌輸出格式爲"%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"。

- logback的configuration

logback的核心零件應當是Appender、Logger、Pattern,在這之前先簡單瞭解一下logback的
< configuration>,< configuration>只有三個屬性:

  • scan:當scan被設置爲true時,當配置文件發生改變,將會被重新加載,默認爲true
  • scanPeriod:檢測配置文件是否有修改的時間間隔,如果沒有給出時間單位,默認爲毫秒,當scan=true時這個值生效,默認時間間隔爲1分鐘
  • debug:當被設置爲true時,將打印出logback內部日誌信息,實時查看logback運行信息,默認爲false
- logback的 logger 與 root

先從最基本的< logger>與< root>開始。
< logger>用來設置某一個包或者具體某一個類的日誌打印級別、以及指定< appender>。< logger>可以包含零個或者多個< appender-ref>元素,標識這個appender將會添加到這個logger。< logger>僅有一個name屬性、一個可選的level屬性和一個可選的additivity屬性:

  • name:用來指定受此logger約束的某一個包或者具體的某一個類
  • level:用來設置打印級別,五個常用打印級別從低至高依次爲TRACE、DEBUG、INFO、WARN、ERROR,如果未設置此級別,那麼當前logger會繼承上級的級別
  • additivity:是否向上級logger傳遞打印信息,默認爲true
    < root>也是< logger>元素,但是它是根logger,只有一個level屬性,因爲它的name就是ROOT,源碼在LoggerContext中:
 1 public LoggerContext() {
 2     super();
 3     this.loggerCache = new ConcurrentHashMap<String, Logger>();
 4 
 5     this.loggerContextRemoteView = new LoggerContextVO(this);
 6     this.root = new Logger(Logger.ROOT_LOGGER_NAME, null, this);
 7     this.root.setLevel(Level.DEBUG);
 8     loggerCache.put(Logger.ROOT_LOGGER_NAME, root);
 9     initEvaluatorMap();
10     size = 1;
11     this.frameworkPackages = new ArrayList<String>();
12 }

Logger的構造函數爲:

Logger(String name, Logger parent, LoggerContext loggerContext) {
    this.name = name;
    this.parent = parent;
    this.loggerContext = loggerContext;
}

看到第一個參數就是Root的name,而這個Logger.ROOT_LOGGER_NAME的定義爲final public String ROOT_LOGGER_NAME = “ROOT”,由此可以看出< root>節點的name就是"ROOT"。
接着寫一段代碼來測試一下:

 1 public class Slf4jTest {
 2 
 3     @Test
 4     public void testSlf4j() {
 5         Logger logger = LoggerFactory.getLogger(Object.class);
 6         logger.trace("=====trace=====");  
 7         logger.debug("=====debug=====");  
 8         logger.info("=====info=====");  
 9         logger.warn("=====warn=====");  
10         logger.error("=====error=====");  
11     }
12     
13 }

logback.xml的配置爲:

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <configuration scan="false" scanPeriod="60000" debug="false">
 3     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 4         <layout class="ch.qos.logback.classic.PatternLayout">
 5             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
 6         </layout>
 7     </appender>
 8     
 9     <root level="info">
10         <appender-ref ref="STDOUT" />
11     </root>
12     
13 </configuration>

root將打印級別設置爲"info"級別,< appender>暫時不管,控制檯的輸出爲:

2020-01-27 02:20:04.779 [main] INFO  java.lang.Object - =====info=====
2020-01-27 02:20:04.782 [main] WARN  java.lang.Object - =====warn=====
2020-01-27 02:20:04.782 [main] ERROR java.lang.Object - =====error=====

logback.xml的意思是,當Test方法運行時,root節點將日誌級別大於等於info的交給已經配置好的名爲"STDOUT"的< appender>進行處理,"STDOUT"將信息打印到控制檯上。

接着理解一下< logger>節點的作用,logback.xml修改一下,加入一個只有name屬性的< logger>:

1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <configuration scan="false" scanPeriod="60000" debug="false">
 3 
 4     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 5         <layout class="ch.qos.logback.classic.PatternLayout">
 6             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
 7         </layout>
 8     </appender>
 9      
10     <logger name="java" />
11       
12      <root level="debug">
13          <appender-ref ref="STDOUT" />
14      </root>
15       
16 </configuration>

注意這個name表示的是LoggerFactory.getLogger(XXX.class),XXX的包路徑,包路徑越少越是父級,我們測試代碼裏面是Object.class,即name="java"是name="java.lang"的父級,root是所有的父級。看一下輸出爲:

2020-01-27 02:20:04.963 [main] DEBUG java.lang.Object - =====debug=====
2020-01-27 02:20:04.965 [main] INFO  java.lang.Object - =====info=====
2020-01-27 02:20:04.966 [main] WARN  java.lang.Object - =====warn=====
2020-01-27 02:20:04.966 [main] ERROR java.lang.Object - =====error=====

出現這樣的結果是因爲:

  • < logger>中沒有配置level,即繼承父級的level,< logger>的父級爲,那麼level=debug
  • 沒有配置additivity,那麼additivity=true,表示此< logger>的打印信息向父級< root>傳遞
  • 沒有配置< appender-ref>,表示此< logger>不會打印出任何信息
    由此可知,< logger>的打印信息向< root>傳遞,< root>使用"STDOUT"這個< appender>打印出所有大於等於debug級別的日誌。舉一反三,我們將< logger>的additivity配置爲false,那麼控制檯應該不會打印出任何日誌,因爲< logger>的打印信息不會向父級< root>傳遞且< logger>沒有配置任何< appender>,大家可以自己試驗一下。

接着,我們再配置一個< logger>:

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <configuration scan="false" scanPeriod="60000" debug="false">
 3  
 4     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 5         <layout class="ch.qos.logback.classic.PatternLayout">
 6             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
 7         </layout>
 8     </appender>
 9      
10     <logger name="java" additivity="false" />
11     <logger name="java.lang" level="warn">
12         <appender-ref ref="STDOUT" />
13     </logger>
14     
15     <root level="debug">
16         <appender-ref ref="STDOUT" />
17     </root>
18      
19 </configuration>

如果讀懂了上面的例子,那麼這個例子應當很好理解:

  • LoggerFactory.getLogger(Object.class),首先找到name="java.lang"這個,將日誌級別大於等於warn的使用"STDOUT"這個打印出來
  • name="java.lang"這個沒有配置additivity,那麼additivity=true,打印信息向上傳遞,傳遞給父級name="java"這個
  • name="java"這個的additivity=false且不關聯任何,那麼name="java"這個不會打印任何信息

由此分析,得出最終的打印結果爲:

2020-01-27 02:22:33.147 [main] WARN  java.lang.Object - =====warn=====
2020-01-27 02:22:33.150 [main] ERROR java.lang.Object - =====error=====

舉一反三,上面的name="java"這個< appender>可以把additivity設置爲true試試看是什麼結果,如果對前面的分析理解的朋友應該很容易想到,有兩部分日誌輸出,一部分是日誌級別大於等於warn的、一部分是日誌級別大於等於debug的。

- logback的 appender

接着看一下< appender >,< appender >是< configuration >的子節點,是負責寫日誌的組件。< appender >有兩個必要屬性name和class:

  • name指定< appender >的名稱
  • class指定< appender >的全限定名
    < appender >有好幾種,上面我們演示過的是ConsoleAppender,ConsoleAppender的作用是將日誌輸出到控制檯,配置示例爲:
1 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
2     <encoder>
3         <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
4     </encoder>
5 </appender>

其中,encoder表示對參數進行格式化。我們和上一部分的例子對比一下,發現這裏是有所區別的,上面使用了< layout >定義< pattern >,這裏使用了< encoder >定義< pattern >,簡單說一下:

  • < encoder >是0.9.19版本之後引進的,以前的版本使用< layout >,logback極力推薦的是使用< encoder >而不是< layout >
  • 最常用的FileAppender和它的子類的期望是使用< encoder >而不再使用< layout >
    關於< encoder >中的格式下一部分再說。接着我們看一下FileAppender,FileAppender的作用是將日誌寫到文件中,配置示例爲:
1 <appender name="FILE" class="ch.qos.logback.core.FileAppender">  
2     <file>D:/123.log</file>  
3     <append>true</append>  
4     <encoder>  
5         <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>  
6     </encoder>  
7 </appender>

它的幾個節點爲:

  • 表示寫入的文件名,可以使相對目錄也可以是絕對目錄,如果上級目錄不存在則自動創建
  • 如果爲true表示日誌被追加到文件結尾,如果是false表示清空文件
  • 表示輸出格式,後面說
  • 如果爲true表示日誌會被安全地寫入文件,即使其他的FileAppender也在向此文件做寫入操作,效率低,默認爲false

接着來看一下RollingFileAppender,RollingFileAppender的作用是滾動記錄文件,先將日誌記錄到指定文件,當符合某個條件時再將日誌記錄到其他文件,RollingFileAppender配置比較靈活,因此使用得更多,示例爲:

1 <appender name="ROLLING-FILE-1" class="ch.qos.logback.core.rolling.RollingFileAppender">   
2     <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">   
3         <fileNamePattern>rolling-file-%d{yyyy-MM-dd}.log</fileNamePattern>   
4         <maxHistory>30</maxHistory>    
5     </rollingPolicy>   
6     <encoder>   
7         <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>   
8     </encoder>   
9 </appender>

這種是僅僅指定了< rollingPolicy >的寫法,< rollingPolicy >的作用是當發生滾動時,定義RollingFileAppender的行爲,其中上面的TimeBasedRollingPolicy是最常用的滾動策略,它根據時間指定滾動策略,既負責滾動也負責觸發滾動,有以下節點:

  • < fileNamePattern >,必要節點,包含文件名及"%d"轉換符,"%d"可以包含一個Java.text.SimpleDateFormat指定的時間格式,如%d{yyyy-MM},如果直接使用%d那麼格式爲yyyy-MM-dd。RollingFileAppender的file子節點可有可無,通過設置file可以爲活動文件和歸檔文件指定不同的位置
  • < maxHistory >,可選節點,控制保留的歸檔文件的最大數量,如果超出數量就刪除舊文件,假設設置每個月滾動且< maxHistory >是6,則只保存最近6個月的文件

想其他還有SizeBasedTriggeringPolicy,用於按照文件大小進行滾動,可以自己查閱一下資料。

- 異步寫日誌

日誌通常來說都以文件形式記錄到磁盤,例如使用,這樣的話一次寫日誌就會發生一次磁盤IO,這對於性能是一種損耗,因此更多的,對於每次請求必打的日誌(例如請求日誌,記錄請求API、參數、請求時間),我們會採取異步寫日誌的方式而不讓此次寫日誌發生磁盤IO,阻塞線程從而造成不必要的性能損耗。(不要小看這個點,可以網上查一下服務端性能優化的文章,只是因爲將日誌改爲異步寫,整個QPS就有了大幅的提高)。
接着我們看下如何使用logback進行異步寫日誌配置:

1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <configuration scan="false" scanPeriod="60000" debug="false">
 3 
 4     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 5         <encoder>
 6             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
 7         </encoder>
 8     </appender>
 9     
10     <appender name="ROLLING-FILE-1" class="ch.qos.logback.core.rolling.RollingFileAppender">   
11         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">   
12               <fileNamePattern>D:/rolling-file-%d{yyyy-MM-dd}.log</fileNamePattern>   
13               <maxHistory>30</maxHistory>    
14         </rollingPolicy>   
15         <encoder>
16               <pattern>%-4relative [%thread] %-5level %lo{35} - %msg%n</pattern>   
17         </encoder>   
18     </appender>
19     
20     <!-- 異步輸出 -->  
21     <appender name ="ASYNC" class= "ch.qos.logback.classic.AsyncAppender">  
22         <!-- 不丟失日誌.默認的,如果隊列的80%已滿,則會丟棄TRACT、DEBUG、INFO級別的日誌 -->  
23         <discardingThreshold>0</discardingThreshold>  
24         <!-- 更改默認的隊列的深度,該值會影響性能.默認值爲256 -->  
25         <queueSize>256</queueSize>  
26         <!-- 添加附加的appender,最多隻能添加一個 -->  
27         <appender-ref ref ="ROLLING-FILE-1"/>  
28     </appender>
29     
30     <logger name="java" additivity="false" />
31     <logger name="java.lang" level="DEBUG">
32         <appender-ref ref="ASYNC" />
33     </logger>
34     
35     <root level="INFO">
36         <appender-ref ref="STDOUT" />
37     </root>
38     
39 </configuration>

即,我們引入了一個AsyncAppender,先說一下AsyncAppender的原理,再說一下幾個參數:

當我們配置了AsyncAppender,系統啓動時會初始化一條名爲"AsyncAppender-Worker-ASYNC"的線程

當Logging Event進入AsyncAppender後,AsyncAppender會調用appender方法,
appender方法中再將event填入Buffer(使用的Buffer爲BlockingQueue,具體實現爲ArrayBlockingQueye)前,
會先判斷當前Buffer的容量以及丟棄日誌特性是否開啓,當消費能力不如生產能力時,
AsyncAppender會將超出Buffer容量的Logging Event的級別進行丟棄,
作爲消費速度一旦跟不上生產速度導致Buffer溢出處理的一種方式。

上面的線程的作用,就是從Buffer中取出Event,交給對應的appender進行後面的日誌推送

從上面的描述我們可以看出,AsyncAppender並不處理日誌,只是將日誌緩衝到一個BlockingQueue裏面去,
並在內部創建一個工作線程從隊列頭部獲取日誌,之後將獲取的日誌循環記錄到附加的其他appender上去,
從而達到不阻塞主線程的效果。因此AsyncAppender僅僅充當的是事件轉發器,
必須引用另外一個appender來做事。

從上述原理,我們就能比較清晰地理解幾個參數的作用了:

  • discardingThreshold,假如等於20則表示,表示當還剩20%容量時,將丟棄TRACE、DEBUG、INFO級別的Event,只保留WARN與ERROR級別的Event,爲了保留所有的events,可以將這個值設置爲0,默認值爲queueSize/5
  • queueSize比較好理解,BlockingQueue的最大容量,默認爲256
  • includeCallerData表示是否提取調用者數據,這個值被設置爲true的代價是相當昂貴的,爲了提升性能,默認當event被加入BlockingQueue時,event關聯的調用者數據不會被提取,只有線程名這些比較簡單的數據
  • appender-ref表示AsyncAppender使用哪個具體的進行日誌輸出
- - < encoder >

< encoder >節點負責兩件事情:

  • 把日誌信息轉換爲字節數組
  • 把字節數組寫到輸出流

目前PatternLayoutEncoder是唯一有用的且默認的encoder,有一個節點,就像上面演示的,用來設置日誌的輸入格式,使用“%+轉換符"的方式,如果要輸出"%“則必須使用”%“對”%"進行轉義。

- - < filter >

最後來看一下< filter >,< filter >是< appender >的一個子節點,表示在當前給到的日誌級別下再進行一次過濾,最基本的Filter有ch.qos.logback.classic.filter.LevelFilter和ch.qos.logback.classic.filter.ThresholdFilter,首先看一下LevelFilter:

1 <configuration scan="false" scanPeriod="60000" debug="false">
 2 
 3     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 4         <encoder>
 5             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
 6         </encoder>
 7         <filter class="ch.qos.logback.classic.filter.LevelFilter">
 8             <level>WARN</level>
 9             <onMatch>ACCEPT</onMatch>
10             <onMismatch>DENY</onMismatch>
11         </filter>
12     </appender>
13     
14     <logger name="java" additivity="false" />
15     <logger name="java.lang" level="DEBUG">
16         <appender-ref ref="STDOUT" />
17     </logger>
18     
19     <root level="INFO">
20         <appender-ref ref="STDOUT" />
21     </root>
22     
23 </configuration>

看一下輸出:

2020-01-27 02:30:08.843 [main] WARN  java.lang.Object - =====warn=====

看到儘管< logge r>配置的是DEBUG,但是輸出的只有warn,因爲在中對匹配到WARN級別時做了ACCEPT(接受),對未匹配到WARN級別時做了DENY(拒絕),當然只能打印出WARN級別的日誌。
再看一下ThresholdFilter,配置爲:

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <configuration scan="false" scanPeriod="60000" debug="false">
 3 
 4     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 5         <encoder>
 6             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
 7         </encoder>
 8         <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
 9             <level>INFO</level>
10         </filter>
11     </appender>
12     
13     <logger name="java" additivity="false" />
14     <logger name="java.lang" level="DEBUG">
15         <appender-ref ref="STDOUT" />
16     </logger>
17     
18     <root level="INFO">
19         <appender-ref ref="STDOUT" />
20     </root>
21     
22 </configuration>

看一下輸出爲:

2020-01-27 02:30:45.353 [main] INFO  java.lang.Object - =====info=====
2020-01-27 02:30:45.358 [main] WARN  java.lang.Object - =====warn=====
2020-01-27 02:30:45.359 [main] ERROR java.lang.Object - =====error=====

因爲ThresholdFilter的策略是,會將日誌級別小於的全部進行過濾,因此雖然指定了DEBUG級別,但是隻有INFO及以上級別的才能被打印出來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章