各位新年快樂,過了個新年,休(hua)息(shui)了三週,不過我又回來更新了,通過前面四篇想必小夥伴已經瞭解日誌的使用以及最佳實踐了,這個系列的文章也差很少要結束了,今天咱們來總結一下。html
這篇文章咱們討論一下 SLF4j 的設計,以及 SLF4j 好在哪,以後進行一些答疑與前系列文章勘誤,最最後咱們來了解一下如何正確的分文件輸出日誌。java
SLF4j 並無使用網上所謂的編譯時綁定,其實是採用了約定俗成的方式,如何作的?很簡單,就是直接加載org/slf4j/impl/StaticLoggerBinder.class
,找到一個直接使用,沒找到或者找到多個報警告,,分析一下源碼:android
咱們一塊兒來看一個個 Logger 實例是如何建立的:git
org.slf4j.LoggerFactory#getLogger(java.lang.String)
,獲取 logger 實例的真正入口ch.qos.logback.classic.LoggerContext#getLogger(java.lang.String)
,調用了 logback 的LoggerContext
(實現 LoggerFactory),具體如何調用到這裏下面解析)childLogger = logger.createChildByName(childName);
建立了 loggger實例,繼續跟進ch.qos.logback.classic.Logger#createChildByName
方法中能夠看到childLogger = new Logger(childName, this, this.loggerContext);
,至此咱們目的也達到了,logger 是 new 出來的並非所謂的編譯時綁定。咱們繼續來跟蹤如何調用到 logback 的 LoggerContext
(LogggerFactory),而且來驗證一下是否真的是所謂的編譯時綁定:github
仍是org.slf4j.LoggerFactory#getLogger(java.lang.String)
方法,此次咱們跟進到org.slf4j.LoggerFactory#getILoggerFactory
方法中發現調用了performInitialization
,跟進去發現調用了bind
,繼續跟進發現調用了findPossibleStaticLoggerBinderPathSet
方法在當前ClassPath下查詢了全部名爲org/slf4j/impl/StaticLoggerBinder.class
類路徑返回面試
真正代碼以下,註釋寫的很明確:算法
Set<URL> staticLoggerBinderPathSet = null;
// skip check under android, see also
// http://jira.qos.ch/browse/SLF4J-328
if (!isAndroid()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
StaticLoggerBinder.getSingleton();
複製代碼
StaticLoggerBinder
這個類就是綁定的關鍵,點進去發現根本不是 SLF4j 的類,而是來自於 Logback,也就是說,SLF4j 使用了第三方(Logback、Log4j 等)提供的中介類,(Spring Boot 自動配置也部分使用了這種思想,之後的全棧系列文章將會有詳細解析,歡迎關注),若是出出現NoClassDefFoundError
則提示一下使用者,而後再也不處理日誌。spring
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
複製代碼
給出結論以前咱們先來明確一下 Java 的綁定(Binding)的概念,Java 自己只支持靜態(static)綁定與運行時(runtime)綁定,直到與 JDK 1.6 版本一塊兒發佈的 JSR269 才能進行編譯時綁定,員外理解的編譯時綁定相似於 lomok 在編譯過程當中修改字節碼。SFL4j 的 logger 實例是 new 出來的,綁定 LogContext
的 StaticLoggerBinder
(中介類) 是寫死的,編譯時並無處理任何邏輯,也談不上什麼編譯時綁定,員外翻遍了 SLF4j 文檔也沒有找到任何有關編譯時綁定的材料,官方只提到了 「static binding」, 因此回到文章標題,網上流傳的編譯時綁定根本就是錯的,SLF4j使用的是 Convention over Configuration(CoC)– 慣例優於配置原則,我無論你是什麼日誌框架,我只加載org.slf4j.impl.StaticLoggerBinder
。這完美契合了軟件設計的 KISS(Keep It Simple, Stupid)原則,而 Commons-logging 魔法(magic)同樣的動態加載雖然設計很高大上,在應用領域卻直接被打臉,低效率、與 OSGi 共同使用所致使的 ClassLoader 問題更是火上澆油,因此員外與你們共勉,寫代碼切勿炫技。apache
以上是本文核心,略過的讀者勞煩再讀一次。架構
先了解一下爲何說 SLF4j 更好,下面兩段話來自於Spring 4.x 官方文檔:
Not Using Commons Logging
Unfortunately, the runtime discovery algorithm in
commons-logging
, while convenient for the end-user, is problematic. If we could turn back the clock and start Spring now as a new project it would use a different logging dependency. The first choice would probably be the Simple Logging Facade for Java ( SLF4J), which is also used by a lot of other tools that people use with Spring inside their applications.不幸的是,
commons-logging
的運行時發現算法雖然對用戶很方便,但卻有問題。 若是咱們有後悔藥可以將 Spring 做爲一個新項目從新啓動,首選多是 Simple Logging Facade for Java(SLF4J),Spring 所依賴的其餘工具也能使用它。
That might seem like a lot of dependencies just to get some logging. Well it is, but it is optional, and it should behave better than the vanilla
commons-logging
with respect to classloader issues, notably if you are in a strict container like an OSGi platform. Allegedly there is also a performance benefit because the bindings are at compile-time not runtime.這看起來好像僅僅爲了日誌就須要不少依賴。但這些依賴都是可選的,在類加載器問題方面,它應該比普通的 Commons-logging 表現得更好,特別是若是您在 OSGi 平臺這樣的嚴格容器中。聽說性能還有優點,由於綁定是在編譯時而不是運行時。
這兩段文字可謂是肺腑之言,公平公正,員外也沒有處處去驗證,所謂性能優點我認爲做爲static final
級別變量,性能優點也不會太大。員外認爲 SLF4j 本質上更好的緣由在於其提供市面上全部日誌框架的兼容解決方案。
第一篇文章「Java日誌體系竟然這麼複雜?——架構篇」其中 Spring Boot的使用依賴,我寫到「Spring已經寫好了一個log4j2-starter但缺乏橋接包」是不對的,員外出於好奇驗證一下,之因此 Spring 沒有依賴 jcl-over-slf4j 是由於 Spring Boot 2.x 版本之後依賴了其本身實現的 Spring-jcl 橋接,而 1.x 版本則帶有 jcl-over-slf4j 依賴,因此抱歉,個人文章這裏寫錯了,望各位周知。
第二篇文章「五年Java經驗,面試仍是說不出日誌該怎麼寫更好?——日誌規範與最佳實踐篇」其中 Log4j2配置文件那一段有誤,缺了一個名爲 STDOUT 的控制檯Appender,代碼以下:
<Console name="STDOUT">
<PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n"/>
</Console>
複製代碼
第四篇文章「這麼香的日誌動態級別與輸出,你肯定不進來看看?——生產環境動態輸入日誌級別、文件」
這篇文章也是有一個配置文件粘貼錯位置了,不過源碼是正確的,請各位下載github源碼,以源碼爲準。
第一個答疑是讀者的一個小要求,問我能不能寫一個YAML格式的 Log4j2 配置文件,固然能夠了,下面是手寫的,請測試一下再進入生產使用:
Configuration:
status: debug
name: YAMLConfig
properties:
property:
name: baseDir
value: logs
appenders:
RollingFile:
- name: RollingFile
fileName: ${baseDir}/log.log
filePattern: "${baseDir}/$${date:yyyy-MM}/log-%d{yyyy-MM-dd-HH}-%i.log.gz"
PatternLayout:
pattern: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p %pid --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}"
Policies:
- TimeBasedTriggeringPolicy: true
SizeBasedTriggeringPolicy:
size: 250 MB
DefaultRollOverStrategy:
max: 100
Delete:
basePath: ${baseDir}
maxDepth: 2
IfFileName:
glob: "*/app-*.log.gz"
IfLastModified:
age: 30d
IfAny:
IfAccumulatedFileSize:
exceeds: 100 GB
IfAccumulatedFileCount:
exceeds: 10
Console:
name: STDOUT
PatternLayout:
Pattern: "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n"
Filters:
ThresholdFilter:
level: debug
Loggers:
logger:
- name: org.apache.logging.log4j.test2
level: debug
additivity: false
AppenderRef:
ref: RollingFile
Root:
level: trace
AppenderRef:
ref: STDOUT
複製代碼
另一個小夥伴在個人文章下提出了幾個問題:
每一個日誌文件大小,如何分割?日期分割?一天一個? 是一天一個目錄仍是一天一個文件? 仍是一週一個目錄? 區不區分error和info日誌在不一樣文件?打不打印其餘級別日誌? 能不能動態修改日誌級別不停機?是否須要異步日誌,大家的訪問量到了默認同步日誌扛不住的地步了麼?怎麼異步日誌?
雖然這個小夥伴態度不是很好,可是問題仍是很好的:
每一個日誌文件大小,如何分割?日期分割?一天一個? 是一天一個目錄仍是一天一個文件? 仍是一週一個目錄?
每一個文件大小我喜歡250M這個數字,也這麼配的,日期分割這個就不該該我來說了,我說一個月一分割,通常應用都好幾百個G了,我說一分鐘一分割,好多應用還不到1M,因此按照本身線上的需求慢慢調整才行。
區不區分error和info日誌在不一樣文件?
員外堅定反對按照日誌級別分文件,設想一下回溯現場的時候,info、warn、error 級別都是有用日誌,若是分開了,是否是逐個去看?若是讓我逐個去定位錯誤位置,我想我會罵孃的,至於如何正確的分文件輸出日誌,後面我會有補充,見下文。
打不打印其餘級別日誌?
打不打印其餘級別日誌根本就是個僞問題,不須要打印其餘級別也就不須要那麼多日誌級別了,這個問題是否是能夠理解爲日誌應該開到什麼級別,我通常開 info 級別,我也見過線上只開 error 的,而後業務裏的日誌輸出都是error的(反面教材)。
能不能動態修改日誌級別不停機?
能的,參考我上一篇文章,並且這方面應該沒有人作的比我文章裏寫的更好了。
是否須要異步日誌,怎麼異步日誌?
我我的不傾向於異步日誌,磁盤IO滿了,開了異步也是緩衝區滿,緩衝區滿了要麼阻塞,要麼拋棄,至於開了異步所帶來的性能優點並不大。怎麼異步日誌我文章裏也有寫,請參閱公衆號。
大家的訪問量到了默認同步日誌扛不住的地步了麼?
日誌扛不住了要先考慮是否是過多,若是實在無法減小日誌,就考慮將日誌輸出路徑單獨掛載磁盤、更換更好的磁盤等等。
讀過員外的文章就知道,員外是同意分文件輸出日誌的,不過員外反對按照級別來輸出文件。如何正確的按文件輸出日誌呢?之前文章沒有寫過,這裏來補充一下。
很簡單,配置多個appender,而後能夠按照 loggger 來分文件,代碼以下:
<Logger name="com.jiyuanwai.log.xxx" level="info" additivity="false">
<appender-ref ref="XXXFile"/>
</Logger>
複製代碼
<Logger name="com.jiyuanwai.log.yyy" level="info" additivity="false">
<appender-ref ref="YYYFile"/>
</Logger>
複製代碼
這個卻是很簡單,可是還有一個問題,單個類若是有多種日誌想要輸出到多個位置,該怎麼辦,解決方案有兩種,一個類持有多個 logger 實例:
class A {
static final Logger log = LoggerFactory.getLogger("com.jiyuanwai.log.xxx");
static final Logger log = LoggerFactory.getLogger("com.jiyuanwai.log.yyy");
...
}
複製代碼
這種辦法實現簡單,可是不優雅,咱們來嘗試拿出另一套方案,就是 Maker 配合 Filter 來實現,固然根據之前的文章瞭解到咱們還可使用 Sift 配合 MDC 來實現,但員外不推薦,至於爲何,做爲公衆號粉絲福利能夠關注公衆號回覆 「Sift」 來獲取答案,咱們來繼續看 demo:
// Marker 也能夠考慮 static final
Marker file1 = MarkerFactory.getMarker("file1");
Marker file2 = MarkerFactory.getMarker("file2");
log.info(file1, "A file 1 log.");
log.info(file2, "A file 2 log.");
複製代碼
配置文件以下:
<appender name="FILE1" class="ch.qos.logback.core.FileAppender">
<file>${LOG_PATH}/testFile1.log</file>
<append>true</append>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.OnMarkerEvaluator">
<marker>file1</marker>
</evaluator>
<!-- 不匹配 NEUTRAL不處理,ACCEPT接收,DENY拋棄 -->
<OnMismatch>DENY</OnMismatch>
<!-- 匹配處理方式 NEUTRAL不處理,ACCEPT接收,DENY拋棄 -->
<OnMatch>ACCEPT</OnMatch>
</filter>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="FILE2" class="ch.qos.logback.core.FileAppender">
<file>${LOG_PATH}/testFile2.log</file>
<append>true</append>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.OnMarkerEvaluator">
<!-- 此處能夠配置多個 marker-->
<marker>file2</marker>
</evaluator>
<!-- 不匹配 NEUTRAL不處理,ACCEPT接收,DENY拋棄 -->
<OnMismatch>DENY</OnMismatch>
<!-- 匹配處理方式 NEUTRAL不處理,ACCEPT接收,DENY拋棄 -->
<OnMatch>ACCEPT</OnMatch>
</filter>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<logger name="com.jiyuanwai.logging.LoggingApplication" additivity="false">
<appender-ref ref="FILE1"/>
<appender-ref ref="FILE2"/>
</logger>
複製代碼
以上只是拋磚引玉,日誌分文件輸出還能夠寫更多邏輯,小夥伴須要本身動手發掘。
至此日誌系列就算是告一段落了,若是還有疑問小夥伴能夠留言討論,接下來一系列咱們進入Spring Boot + Vue 的全棧之路,敬請關注。
以上是我的觀點,若是有問題或錯誤,歡迎留言討論指正,碼字不易,若是以爲寫的不錯,求關注、求點贊、求轉發。
掃碼關注公衆號,第一時間得到更新