日志對于日常開發(fā)懦尝、問題解決的重要性不言而喻蝇摸,工作中由于接觸各種類型的項(xiàng)目和框架,看到很多日志的不同用法捌袜,造成項(xiàng)目依賴和代碼的混亂说搅,故梳理總結(jié)。本文包括Java日志體系的發(fā)展歷史虏等、推薦應(yīng)用方式和部分底層原理弄唧;運(yùn)維層面的日志系統(tǒng)搭建及不同組件的對比;及常用的查詢?nèi)罩镜腖inux命令霍衫,方便Linux機(jī)器上日志的查詢候引。
@[TOC]
一. Java日志體系
Java日志體系伴隨著Java語言的發(fā)展,同時也夾雜著開發(fā)者之間敦跌、組織之間的較量背伴,一直至今。
1.1. Java日志體系發(fā)展歷史及相互關(guān)聯(lián)
1.1.1. System.out 和 System.err
Java語言自1995年向互聯(lián)網(wǎng)公開峰髓,作為一門編程語言,就如C語言的 printf("Hello, World!");
一樣息尺,Java當(dāng)然也有內(nèi)置的輸出方式携兵,即 System.out.println()
和 System.err.println()
,前者指向標(biāo)準(zhǔn)輸出搂誉,后者指向標(biāo)準(zhǔn)錯誤輸出徐紧。在日志工具出現(xiàn)之前,大部分是用這種原始的方式打印查看日志的。
1.1.2. Log4j
Apache Log4j 是一個基于Java的日志記錄工具并级。它是由 Ceki Gülcü 首創(chuàng)的拂檩,于2001年初推出后備受歡迎,后來成為 Apache 基金會項(xiàng)目中的一員嘲碧,一度成為 Java 日志的標(biāo)桿稻励。傳言 Apache 基金會曾建議 Sun 將 Log4j 引入 Java 標(biāo)準(zhǔn)類庫,但是被拒絕了愈涩。
1.1.3. JUL
2002年2月發(fā)布的 JDK1.4 中望抽,Sun 推出了自己的日志庫,java.util.logging履婉,很多實(shí)現(xiàn)方法都是仿照 Log4j煤篙,雖然不太有風(fēng)度,但是此后打日志有了兩種方式毁腿。
1.1.4. JCL
JCL全稱 Apache Commons Logging辑奈,據(jù)說之前叫 Jakarta Commons Logging,于2002年8月由 Apache 發(fā)布已烤。不同于 Log4j 和 JUL鸠窗,JCL是一種日志門面(Logging Facade),只提供 Log API草戈,不提供實(shí)現(xiàn)塌鸯。
理想上是很優(yōu)雅的,大家記錄日志都使用 JCL 的接口唐片,運(yùn)行時可以按照自己的需求(或者喜好)來選擇使用合適的Log Implementation丙猬。如果用Log4j,就添加 Log4j 的 jar 包進(jìn)去费韭,然后寫一個 Log4j 的配置文件茧球;如果喜歡用JUL,就只需要寫個 JUL 的配置文件星持。如果有其他的新的日志庫出現(xiàn)抢埋,也只需要它提供一個Adapter,運(yùn)行的時候把這個日志庫的 jar 包加進(jìn)去督暂。這也是面向接口編程思想的體現(xiàn)揪垄,但是由于運(yùn)行時動態(tài)綁定等原因,實(shí)際使用中出現(xiàn)了性能問題及類加載等問題逻翁。
1.1.5. SLF4J
SLF4J 全稱 Simple Logging Facade for Java饥努。雖然 JCL 在設(shè)計上想法很好,但是由于其造成的一系列問題八回,Ceki Gülcü 大神表示強(qiáng)烈不滿酷愧,此時他已經(jīng)離開了 Apache驾诈,于是在2005年推出了自創(chuàng)的日志門面,即SLF4J溶浴。但是由于 Log4j 和 JUL 已經(jīng)在那里了乍迄,而且不是按照 SLF4J 的 API 實(shí)現(xiàn)的,所以存在兼容性的問題士败,SLF4J 面臨了只有 API 沒有實(shí)現(xiàn)的局面闯两。此時 Ceki Gülcü 推出了 橋接包,如果需要使用某一種日志實(shí)現(xiàn)拱烁,那么選擇相對應(yīng)的 SLF4J 的橋接包即可生蚁。比如使用 log4j 日志組件,就加入 slf4j-log4j12 橋接包戏自。
不得不說 Ceki Gülcü 野心很大邦投,為了一統(tǒng)江湖甚至推出了 slf4j-jcl 橋接包,把作為日志門面的 JCL 也視為了他可以匹配的“實(shí)現(xiàn)”擅笔。
現(xiàn)在江湖變成了這樣的:
但是還存在一個問題志衣,
1.1.6. Logback
1.2. Java日志相關(guān)部分原理
1.2.1. System.out.println()
初學(xué)時會像記公式一樣背下來這一串代碼,沒有按照J(rèn)ava方法調(diào)用的角度去理解猛们,先來刨一刨這個方法念脯。
- System 是 java.lang 包中的一個 final 類:/classes/java/lang/System.java。根據(jù) JavaDoc弯淘,“ System 類提供的包括:標(biāo)準(zhǔn)輸入绿店,標(biāo)準(zhǔn)輸出和錯誤輸出流;訪問外部定義的屬性和環(huán)境變量庐橙;一種加載文件和庫的方法假勿;以及用于快速復(fù)制陣列的一部分的實(shí)用方法√睿”
- out 是 System 類的靜態(tài)成員字段转培,類型為 PrintStream。它在啟動時就會被實(shí)例化浆竭,并與主機(jī)的標(biāo)準(zhǔn)輸出控制臺進(jìn)行映射浸须。該流在實(shí)例化之后立即打開,并準(zhǔn)備接受數(shù)據(jù)邦泄。其在 System 類中的定義語句如下:
/**
* The "standard" output stream. This stream is already
* open and ready to accept output data. Typically this stream
* corresponds to display output or another output destination
* specified by the host environment or user.
*/
public final static PrintStream out = null;
- println 是 PrintStream 類的一個方法删窒。println 打印(參數(shù)內(nèi)容 + 換行符)到控制臺顺囊。PrintStream 類中有多個重載的 println 方法易稠。每個 println 是通過調(diào)用 print 方法并添加一個換行符實(shí)現(xiàn)的。print 方法是通過調(diào)用流的 write 方法實(shí)現(xiàn)的包蓝。
了解了 System.out.println() 的表層原理驶社,System.err.println() 和其幾乎相同,err 也是System 類的一個 PrintStream 類型成員测萎。
但是不解的問題仍然存在亡电。
- System.out 這個變量被 final 修飾,初始化為 null硅瞧,為何執(zhí)行 println() 方法不報空指針異常份乒?
- System.out 是如何被指向標(biāo)準(zhǔn)輸出(默認(rèn)控制臺)的;
- out 變量被聲明為 final腕唧,是否意味著 System.out.println() 方法只能向控制臺打踊蛳健;
為了解決這些疑惑枣接,深入理解 System.out.println() 調(diào)用的原理:
System 類在 vm 啟動后優(yōu)先初始化颂暇,比 out 對象的類型 PrintStream 靠前,而 out 對象是 static final 修飾的但惶,定義的時候必須初始化耳鸯,故在定義時初始化為 null ;
想修改已經(jīng)被賦值的 final 變量膀曾,可用的一種方法是在沒有內(nèi)聯(lián)優(yōu)化的情況下利用反射將 final 修飾符去掉再設(shè)置新值县爬,但是這對 System 類不可行。故 Java 采用更底層的方法繞過 final 限制添谊;
-
絕大部分博客财喳,包括所有筆者瀏覽過的中文博客中都陳述:System 類有唯一一個 static 代碼段執(zhí)行的方法:registerNatives(),源碼見 /native/java/lang/System.c#l47斩狱。此方法中調(diào)用了該類的 initializeSystemClass() 方法做一些初始化工作耳高,依據(jù)是 Java 源碼這個方法上的注釋內(nèi)容,如下圖喊废。但是此解釋是完全錯誤的祝高。
image.png
筆者翻看了和 registerNatives() 方法有關(guān)的所有 hotspot 源碼,沒有找到 initializeSystemClass() 方法的調(diào)用邏輯污筷。registerNatives() 方法是一個常見于各種類中的工闺,用于初始化該類的 native 方法的方法。如在 System 類中瓣蛀,有三個 native 方法需要初始化陆蟆,如下圖。
image.png
這里的 *env 為 JNI 環(huán)境惋增, 方法進(jìn)入 jni_RegisterNatives():/vm/prims/jni.cpp#l4050叠殷,代碼邏輯就是遍歷了上圖中的 methods 數(shù)組,注冊了 native 方法诈皿。和我們所關(guān)心的 out 對象一點(diǎn)關(guān)系都沒有林束,不多說像棘。 -
實(shí)際上 initializeSystemClass() 方法是在 /vm/runtime/thread.cpp#l3529 中被調(diào)用的,在初始化 System 類的同時也初始化了很多其他基礎(chǔ)類壶冒,如 Class缕题、Method、Finalizer 等胖腾,只不過初始化 System 類用了一個專門的方法烟零。
image.png
此方法內(nèi)邏輯是,先解析出 System 類元數(shù)據(jù) Klass咸作,再用 call_static 調(diào)用已被編譯的 initializeSystemClass() 方法锨阿。整體在 thread 初始化后進(jìn)行。 -
破案了记罚∈睿回到 Java 中的 initializeSystemClass() 方法,該方法對 in毫胜、out书斜、err 三個流進(jìn)行了初始化:
image.png
其中,F(xiàn)ileDescriptor 是文件描述符酵使,對于每個打開的文件荐吉,操作系統(tǒng)都會分配一個文件描述符,0口渔、1样屠、2 文件描述符分別預(yù)留給標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出缺脉、錯誤輸出痪欲,System.out.println() 最終是在標(biāo)準(zhǔn)輸出文件描述符上執(zhí)行 write() 操作,in 和 err 同理攻礼,故在初始化的時候傳入了參數(shù)0业踢、1、2:
public static final FileDescriptor in = new FileDescriptor(0);
public static final FileDescriptor out = new FileDescriptor(1);
public static final FileDescriptor err = new FileDescriptor(2);
-
注意礁扮,這里 setOut0() 方法也是 native 方法知举,實(shí)現(xiàn)在 /native/java/lang/System.c#l464。首先
(*env)->GetStaticFieldID(env,cla,"out","Ljava/io/PrintStream;")
獲取 System.java 的靜態(tài)成員 out 的 jfieldID太伊,(*env)->SetStaticObjectField(env,cla,fid,stream)
設(shè)置 fid(即 out 的 jfieldID)對應(yīng)的靜態(tài)成員的值為傳入的 stream雇锡。源碼在注釋中也大方說明了這么做的原因:
image.png -
System 類中提供了 setOut() 方法用于修改標(biāo)準(zhǔn)輸出,setOut() 方法也是調(diào)用了 setOut0() 才得以實(shí)現(xiàn)僚焦,在此之前還會先檢查是否有 setIO 的權(quán)限:
image.png
故只要在 Java 代碼中調(diào)用 setOut() 方法即可修改 System.out.println() 的輸出目標(biāo)锰提,舉例如下:
image.png 至此知道了 out 對象的來龍去脈。關(guān)于那個大家都誤解的注釋,個人認(rèn)為那個注釋應(yīng)該是加在 System 類上的立肘,但被加在了方法上边坤,不仔細(xì)研究很容易跑偏。再后來谅年,Java 改變了 System 類初始化的邏輯惩嘉,變?yōu)榱巳齻€步驟,方法分別取名為
initPhase1
踢故、initPhase2
、initPhase3
惹苗,負(fù)責(zé)不同的功能殿较,out 的初始化在 call_initPhase1() 方法中,詳見 /vm/runtime/thread.cpp#l3376桩蓉。但是 Java 的開發(fā)者依舊忘記了更新那段令人迷惑的注釋淋纲,后來在被提 issue 后進(jìn)行了 update,可以借此看看 Java 的 issue 流程院究,也是趣事:https://bugs.openjdk.java.net/browse/JDK-8232617洽瞬。-
如果想給控制臺輸出自定義一些格式,也不是不可以业汰,如下圖伙窃。但是這個格式的設(shè)定和 Java 沒什么關(guān)系,參考:ANSI轉(zhuǎn)義序列
image.png