讀
作者日常在與其他同學合作時,經常發現不合理的日志配置以及五花八門的日志記錄方式,后續作者打算在團隊內做一次Java日志的分享,本文是整理出的系列文章第一篇。
序
寫這篇文章的初衷,是想在團隊內做一次Java日志的分享,因為日常在與其他同學合作時,經常發現不合理的日志配置以及五花八門的日志記錄方式。但在準備分享、補充細節的過程中,我又進一步發現目前日志相關的文章,都只是專注于某一個方面,或者講歷史和原理,或者解決包沖突,卻都沒有把整個Java日志知識串聯起來。最終這篇文章超越了之前的定位,越寫越豐富,為了讓大家看得不累,我的文章將以系列的形式展示。
一、前言
日志發展到今天,被抽象成了三層:接口層、實現層、適配層:
適配層又可以分為綁定(Binding)和橋接(Bridging)兩種能力:
如果你覺得上面的描述比較抽象生硬,可以先跳過,等把本篇看完自然就明白了。
接下來我們就以時間順序,回顧一下Java日志的發展史,這有助于指導我們后續的實踐,真正做到知其所以然。
二、歷史演進
2.1 標準輸出 (<1999)
Java最開始并沒有專門記錄日志的工具,大家都是用System.out和System.err輸出日志。但它們只是簡單的信息輸出,無法區分錯誤級別、無法控制輸出粒度,也沒有什么管理、過濾能力。隨著Java工程化的深入,它們的能力就有些捉襟見肘了。
雖然System.out和System.err默認輸出到控制臺,但它們是有能力將輸出保存到文件的:
System.setOut(new PrintStream(new FileOutputStream("log.txt", true)));
System.out.println("這句將輸出到 log.txt 文件中");
System.setErr(new PrintStream(new FileOutputStream("error.txt", true)));
System.err.println("這句將輸出到 error.txt 文件中");
2.2 Log4j (1999)
在1996年,一家名為SEMPER的歐洲公司決定開發一款用于記錄日志的工具。經過多次迭代,最終發展成為Log4j。這款工具的主要作者是一位名叫Ceki Gülcü[2]的俄羅斯程序員,請記住他的名字:Ceki,后面還會多次提到他。
到了1999年,Log4j已經被廣泛使用,隨著用戶規模的增長,用戶訴求也開始多樣化。于是Ceki在2001年選擇將Log4j開源,希望借助社區的力量將Log4j發展壯大。不久之后Apache基金會向Log4j拋出了橄欖枝,自然Ceki也加入Apache繼續從事 Log4j的開發,從此Log4j改名Apache Log4j[3]并進入發展的快車道。
Log4j相比于System.out提供了更強大的能力,甚至很多思想到現在仍被廣泛接受,比如:
隨著Log4j的成功,Apache又孵化了Log4Net[4]、Log4cxx[5]、Log4php[6]產品,開源社區也模仿推出了如Log4c[7]、Log4cpp[8]、Log4perl[9]等眾多項目。從中也可以印證Log4j在日志處理領域的江湖影響力。
不過Log4j有比較明顯的性能短板,在Logback和Log4j 2推出后逐漸式微,最終Apache在2015年宣布終止開發Log4j并全面遷移至Log4j 2[10](可參考【2.7 Log4j 2 (2012)】)。
2.3 JUL (2002.2)
隨著Java工程的發展,Sun也意識到日志記錄非常重要,認為這個能力應該由JRE原生支持。所以在1999年Sun提交了JSR 047[11]提案,標題就叫「Logging API Specification」。不過直到2年后的2002年,Java官方的日志系統才隨Java 1.4發布。這套系統稱做Java Logging API,包路徑是java.util.logging,簡稱JUL。
在某些追溯歷史的文章中提到,「Apache曾希望將 Log4j加入到JRE中作為默認日志實現,但傲慢的Sun沒有答應,反而很快推出了自己的日志系統」。對于這個說法我并沒有找到出處,無法確認其真實性。
不過從實際推出的產品來看,更晚面世的JUL無論是功能還是性能都落后于Log4j,頗有因被寄予厚望而倉促發布的味道,也許那個八卦并非空穴來風,哈哈。雖然在2004年推出的Java 5.0 (1.5) [12]上JUL進步不小,但它在Log4j面前仍無太多亮點,廣大開發者并沒有遷移的動力,導致JUL始終未成氣候。
我們在后文沒有推薦JUL的計劃,所以這里也不多介紹了(主要是我也不會)。
2.4 JCL (2002.8)
在Log4j和JUL之外,當時市面上還有像Apache Avalon[13](一套服務端開發框架)、 Lumberjack[14](一套跑在JDK 1.2/1.3上的開源日志工具)等日志工具。
對于獨立且輕量的項目來說,開發者可以根據喜好使用某個日志方案即可。但更多情況是一套業務系統依賴了大量的三方工具,而眾多三方工具會各自使用不同的日志實現,當它們被集成在一起時,必然導致日志記錄混亂。
為此Apache在2002年推出了一套接口Jakarta Commons Logging[15],簡稱 JCL,它的主要作者仍然是Ceki。這套接口主動支持了Log4j、JUL、Apache Avalon、Lumberjack等眾多日志工具。開發者如果想打印日志,只需調用JCL的接口即可,至于最終使用的日志實現則由最上層的業務系統決定。我們可以看到,這其實就是典型的接口與實現分離設計。
但因為是先有的實現(Log4j、JUL)后有的接口(JCL),所以JCL配套提供了接口與實現的適配層(沒有使用它的最新版,原因會在【1.2.7 Log4j2 (2012)】提到):
簡單介紹一下JCL自帶的幾個適配層/實現層:
當時項目前綴取名Jakarta,是因為它屬于Apache與Sun共同推出的Jakarta Project[16]項目(郵件[17])。現在JCL作為Apache Commons[18]的子項目,叫 Apache Commons Logging,與我們常用的Commons Lang[19]、Commons Collections [20]等是師兄弟。但JCL的簡寫命名被保留了下來,并沒有改為ACL。
2.5 Slf4j (2005)
Log4j的作者Ceki看到了很多Log4j和JCL的不足,但又無力推動項目快速迭代,加上對Apache的管理不滿,認為自己失去了對Log4j項目的控制權(博客[21]、郵件[22]),于是在2005年選擇自立門戶,并很快推出了一款新作品Simple Logging Facade for Java[23],簡稱Slf4j。
Slf4j也是一個接口層,接口設計與JCL非常接近(畢竟有師承關系)。相比JCL有一個重要的區別是日志實現層的綁定方式:JCL是動態綁定,即在運行時執行日志記錄時判定合適的日志實現;而Slf4j選擇的是靜態綁定,應用編譯時已經確定日志實現,性能自然更好。這就是常被提到的classloader問題,更詳細地討論可以參考What is the issue with the runtime discovery algorithm of Apache Commons Logging[24]以及Ceki自己寫的文章Taxonomy of class loader problems encountered when using Jakarta Commons Logging[25]。
在推出Slf4j的時候,市面上已經有了另一套接口層JCL,為了將選擇權交給用戶(我猜也為了挖JCL的墻角),Slf4j推出了兩個橋接層:
Slf4j通過推出各種適配層,基本滿足了用戶的所有場景,我們來看一下它的全家桶:
網上介紹Slf4j的文章,經常會引用它官網上的兩張圖:
感興趣的同學也可以參考。
這里解釋一下slf4j-log4j12這個名字,它表示Slf4j + Log4j 1.2(Log4j的最后一個版本) 的適配層。類似的,slf4j-jdk14表示Slf4j + JDK 1.4(就是 JUL)的適配層。
2.6 Logback (2006)
然而Ceki的目標并不止于Slf4j,面對自己一手創造的Log4j,作為原作者自然是知道它存在哪些問題的。于是在2006年Ceki又推出了一款日志記錄實現方案:Logback[26]。無論是易用度、功能、還是性能,Logback 都要優于Log4j,再加上天然支持Slf4j而不需要額外的適配層,自然擁躉者眾。目前Logback已經成為Java社區最被廣泛接受的日志實現層(Logback自己在2021年的統計是48%的市占率[27])。
相比于Log4j,Logback提供了很多我們現在看起來理所當然的新特性:
Logback主要由三部分組成(網上各種文章在介紹classic和access時都描述的語焉不詳,我不得不直接翻官網文檔找更明確的解釋):
2.7 Log4j 2 (2012)
看著Slf4j + Logback搞的風生水起,Apache自然不會坐視不理,終于在2012年憋出一記大招:Apache Log4j 2[29],它自然也有不少亮點:
Log4j 2主要由兩部分組成:
你會發現Log4j 2的設計別具一格,提供JCL和Slf4j之外的第三個接口層(log4j-api,雖然只是自己的接口),它在官網API Separation[33]一節中解釋說,這樣設計可以允許用戶在一個項目中同時使用不同的接口層與實現層。
不過目前大家一般把Log4j 2作為實現層看待,并引入JCL或Slf4j作為接口層。特別是JCL,在時隔近十年后,于2023年底推出了1.3.0 版[34],增加了針對Log4j 2的適配。還記得我們在【1.2.4 JCL (2002.8)】中沒有用最新版的JCL做介紹嗎,就是因為這個十年之后的版本把那些已經「作古」的日志適配層@Deprecated掉了。
多說一句,其實Logback和Slf4j就像log4j-core和log4j-api的關系一下,目前如果你想用Logback也只能借助Slf4j。但誰讓它們生逢其時呢,大家就會分別討論認為是兩個產品。
雖然Log4j 2發布至今已有十年(本文寫于2024年),但它仍然無法撼動Logback的江湖地位,我個人總結下來主要有兩點:
比如,曾有人建議Spring Boot將日志系統從Logback切換到Log4j2[35],但被Phil Webb[36](Spring Boot核心貢獻者)否決。他在回復中給出的原因包括:Spring Boot需要保證向前兼容以方便用戶升級,而切換Log4j 2是破壞性的;目前絕大部分用戶并未面臨日志性能問題,Log4j 2所推崇的性能優勢并非框架與用戶的核心關切;以及如果用戶想在Spring Boot中切換到Log4j 2也很方便(如需切換可參考 官方文檔[37])。
2.8 spring-jcl (2017)
因為目前大部分應用都基于Spring/Spring Boot搭建,所以我額外介紹一下spring-jcl [38]這個包,目前Spring Boot用的就是spring-jcl + Logback這套方案。
Spring曾在它的官方Blog《Logging Dependencies in Spring》[39]中提到,如果可以重來,Spring會選擇李白Slf4j而不是JCL作為默認日志接口。
現在Spring又想支持Slf4j,又要保證向前兼容以支持JCL,于是從5.0(Spring Boot 2.0)開始提供了spring-jcl這個包。它頂著Spring的名號,代碼中包名卻與JCL 一致(org.apache.commons.logging),作用自然也與JCL一致,但它額外適配了Slf4j,并將Slf4j放在查找的第一順位,從而做到了「既要又要」(你可以回到【1.2.4 JCL (2002.8)】節做一下對比)。
如果你是基于Spring Initialize [40]新創建的應用,可以不必管這個包,它已經在背后默默工作了;如果你在項目開發過程中遇到包沖突,或者需要自己選擇日志接口和實現,則可以把spring-jcl當作JCL對待,大膽排除即可。
2.9 其他
除了我們上邊提到的日志解決方案,還有一些不那么常見的,比如:
因為這些日志框架我們在實際開發中用的很少,此文也不再贅述了(主要是我也不會)。
三、總結
歷史介紹完了,但故事并沒有結束。兩個接口(JCL、Slf4j)四個實現(Log4j、JUL、Logback、Log4j2),再加上無數的適配層,它們之間串聯成了一個網,我專門畫了一張圖:
解釋/補充一下這張圖:
如果你之前在看「1.1 前言」時覺得過于抽象,那么此時建議你再回頭看一下,相信會有更多體會。
從這段歷史,我也發現了幾個有趣的細節:
參考鏈接:
[1]https://codedocs.org/what-is/david-wheeler-computer-scientist
[2]https://github.com/ceki
[3]https://logging.apache.org/log4j/1.2/
[4]https://logging.apache.org/log4net/
[5]https://logging.apache.org/log4cxx/
[6]https://logging.apache.org/log4php/
[7]https://log4c.sourceforge.net/
[8]https://log4cpp.sourceforge.net/
[9]https://mschilli.github.io/log4perl/
[10]https://news.apache.org/foundation/entry/apache_logging_services_project_announces
[11]https://jcp.org/en/jsr/detail
[12]https://www.java.com/releases/
[13]https://avalon.apache.org/
[14]https://javalogging.sourceforge.net/
[15]https://commons.apache.org/proper/commons-logging/
[16]https://jakarta.apache.org/
[17]https://lists.apache.org/thread/53otcqljjfnvjs3hv8m4ldzlgz59yk6k
[18]https://commons.apache.org/
[19]https://commons.apache.org/proper/commons-lang/
[20]https://commons.apache.org/proper/commons-collections/
[21]http://ceki.blogspot.com/2010/05/forces-and-vulnerabilites-of-apache.html
[22]https://lists.apache.org/thread/dyzmtholjdlf3h32vvl85so8sbj3v0qz
[23]https://www.slf4j.org/
[24]https://stackoverflow.com/questions/3222895/what-is-the-issue-with-the-runtime-discovery-algorithm-of-apache-commons-logging
[25]https://articles.qos.ch/classloader.html
[26]https://logback.qos.ch/
[27]https://qos.ch/
[28]https://logback.qos.ch/access.html
[29]https://logging.apache.org/log4j/2.x/
[30]https://logging.apache.org/log4j/2.x/manual/extending.html
[31]https://logging.apache.org/log4j/2.x/manual/async.html
[32]https://logging.apache.org/log4j/2.x/performance.html
[33]https://logging.apache.org/log4j/2.x/manual/api-separation.html
[34]https://commons.apache.org/proper/commons-logging/changes-report.html
[35]https://github.com/spring-projects/spring-boot/issues/16864
[36]https://spring.io/team/philwebb
[37]https://docs.spring.io/spring-boot/docs/3.2.x/reference/html/howto.html
[38]https://docs.spring.io/spring-framework/reference/core/spring-jcl.html
[39]https://spring.io/blog/2009/12/04/logging-dependencies-in-spring
[40]https://start.spring.io/
[41]https://google.github.io/flogger/
[42]https://github.com/jboss-logging
[43]https://reload4j.qos.ch/
[44]https://github.com/torvalds
[45]https://moolenaar.net/
作者:尚左
來源-微信公眾號:阿里云開發者
出處:https://mp.weixin.qq.com/s/eIiu08fVk194E0BgGL5gow
Postfix有數百種配置選項。 我概述了/etc/postfix/main.cf文件中的一些常見問題。
用于出站郵件的域名
將此選項設置為出站郵件所需的域名。 默認情況下,此選項使用出站電子郵件的主機名。 該選項還指定附加到非限定收件人地址的域。
#default value is: #myorigin=$myhostname #myorigin=$mydomain #we are going to send outbound mail as originating from example.com mydomain=example.com myorigin=$mydomain
此選項指定Postfix接收電子郵件的域名。 默認情況下,Postfix僅接收主機名的電子郵件。 對于域郵件服務器,請更改該值以包含域名。
#default value mydestination=$myhostname, localhost.$mydomain, localhost #we are going to change it so that we can receive email for example.com mydestination=$myhostname, localhost.$mydomain, localhost, $mydomain
默認情況下,Postfix允許Postfix服務器的本地子網上的客戶端將其用作中繼 - 換句話說,即$ mynetworks配置參數中定義的那些網絡。 更改此項以包括組織內應允許使用此Postfix服務器發送電子郵件的所有網絡。
#default value #mynetworks_style=class #mynetworks_style=subnet #mynetworks_style=host #mynetworks=168.100.189.0/28, 127.0.0.0/8 #mynetworks=$config_directory/mynetworks #mynetworks=hash:/etc/postfix/network_table #change the default values to be your network, assuming your network is 10.0.0.0/8 mynetworks=10.0.0.0/8 #another way of accomplishing the above is: mynetworks_style=class #if you want to forward e-mail only from the Postfix host: mynetworks_style=host #if you want only the Postfix server subnet to forward e-mail via the Postfix server: mynetworks_style=subnet
當郵件來自授權網絡之外的客戶端時,Postfix僅將電子郵件轉發給授權域。 您可以使用relay_domains參數指定哪些域可以是未經身份驗證的發件人的收件人域。
#default value is: #relay_domains=$mydestination #if you do not want to forward e-mail from strangers, then change it as follows (recommended for outgoing mail servers, not for incoming): relay_domains=#if you want to forward e-mail from strangers to your domain: relay_domains=$mydomain
Postfix使用收件人的郵件交換器(MX)記錄直接發送郵件。 您可能不需要此功能,因為轉發到過濾出站郵件的外部郵件托管提供商可能更好。
#default value is: #relayhost=$mydomain #relayhost=[gateway.my.domain] #relayhost=[mailserver.isp.tld] #relayhost=uucphost #relayhost=[an.ip.add.ress] #change the value to be relayhost=external.mail.provider.ip.address
您可能想要定義的另一個值是在出現任何問題時向誰發送電子郵件。 postmaster電子郵件地址在/ etc / aliases中指定,而不是在/etc/postfix/main.cf中指定。
#default value is: $ grep -i postmaster /etc/aliases mailer-daemon: postmaster postmaster: root #change to an e-mail address in your org that is an alias for the team responsible for Postfix postmaster: email-admins@example.com
如果Postfix服務器使用NAT(換句話說,它在私有IP空間中),并且它正在接收公共IP地址的電子郵件地址,則還需要指定它。
#default value is: #proxy_interfaces=#proxy_interfaces=1.2.3.4 #change it to your external IP address to which e-mail is sent proxy_interfaces=your.public.email.server.ip.address
Postfix日志記錄通過/etc/rsyslog.conf中的Syslog完成。 默認情況下,通常會將所有郵件日志發送到/ var / log / maillog,這是存儲郵件日志的好地方。
#default value is: mail.* -/var/log/maillog
設置Postfix后,下一個要解決的問題是如何讓客戶端訪問電子郵件。 在客戶端訪問方面,一些流行的協議包括
通常,避免直接傳遞到組織中的目標服務器,以保持簡單; 相反,應用程序服務器從郵箱服務器中提取電子郵件。 應用程序服務器可以使用一個或多個協議,例如IMAP或POP。 您也可以讓最終用戶使用這些協議來訪問其郵箱,而不是應用程序服務器。 IMAP在RFC 3501(http://tools.ietf.org/html/rfc3501)中定義,并且具有比POP更多的功能。 POP在RFC 1939(https://www.ietf.org/rfc/rfc1939.txt)中定義,并且已經存在了很長時間。
如果您有需要提取電子郵件的應用程序服務器,可以提供幫助的潛在設計如圖4-12所示。 郵件傳遞代理(MDA)和郵件提交代理(MSA)都可以是Postfix服務器。 MDA是將郵件發送到郵箱的代理; MSA接受來自郵件用戶代理的電子郵件。
For the MSA, there are numerous options, including
這些MSA選項中的每一個都是開源和免費的。 它們都支持數千個郵箱,可以幫助您輕松管理電子郵件環境。
Postfix支持有很多選項。 在線文檔非常好,與任何開源免費軟件一樣,用戶社區是您閱讀文檔后獲得幫助的最佳選擇。 一些在線幫助選項包括:
版本控制在企業基礎結構中有許多用途。 傳統方法是使用修訂控制來進行源代碼管理。 但是,修訂控制也可以在基礎設施管理中發揮重要作用。 例如,如果在環境中使用BIND,則BIND配置可以存儲在Git中。 Postfix和其他軟件(如OpenVPN,iptables和Hadoop)等軟件的配置也可以存儲在版本控制中。 Puppet和Salt等配置管理工具也可以輕松地與Git集成。
大量的開源軟件源代碼存儲在Git中。 在企業中使用Git的一個優點是與Internet社區的協作變得更加容易。 一些使用Git的主要開源項目包括:
A more complete list is found at https://git.wiki.kernel.org/index.php/GitProjects.
ython的list是一種非常靈活的數據結構,它允許存儲任意類型的有序集合,包括數字、字符串、甚至是其他的list,其強大的特性為Python編程提供了很大的便利。在這個文檔中,我們將介紹Python list的基礎知識,涵蓋了list的定義、創建、訪問、操作等方面的內容。
在Python中,可以使用方括號[]來創建一個空的list,例如:
Copy codea=[]
也可以使用方括號[]初始化一個包含元素的list,例如:
Copy codeb=[1, 2, 3, 4, 5]
c=['apple', 'banana', 'orange']
d=[1, 'hello', 3.14]
list的元素可以是任何類型,而且list中的元素可以是不同的類型,這是它非常靈活的地方。
Python的list可以使用索引來訪問其中的元素,索引從0開始,例如:
Copy codea=[1, 2, 3, 4, 5]
print(a[0]) # 1
print(a[2]) # 3
list還可以使用負數索引來訪問其中的元素,例如:
Copy codeprint(a[-1]) # 5
print(a[-3]) # 3
這樣,我們可以更加方便地訪問list中的元素。
向list中添加元素有兩種方式:使用append()方法添加,或使用+運算符連接兩個list。例如:
Copy codea=['apple', 'banana', 'orange']
a.append('watermelon')
print(a) # ['apple', 'banana', 'orange', 'watermelon']
b=['pear', 'grape']
c=a + b
print(c) # ['apple', 'banana', 'orange', 'watermelon', 'pear', 'grape']
可以使用del語句或remove()方法從list中刪除元素:
Copy codea=['apple', 'banana', 'orange']
del a[0]
print(a) # ['banana', 'orange']
a.remove('banana')
print(a) # ['orange']
我們可以使用切片來訪問list的一個子集合。切片使用[start:stop:step]的格式,例如:
Copy codea=[1, 2, 3, 4, 5]
print(a[1:3]) # [2, 3]
print(a[:3]) # [1, 2, 3]
print(a[2:]) # [3, 4, 5]
print(a[::2]) # [1, 3, 5]
Python提供了兩種方法對list進行排序:sort()方法和sorted()函數。sort()方法會改變原list,而sorted()函數返回一個新的已排序的list。例如:
Copy codea=[3, 1, 4, 2, 5]
a.sort()
print(a) # [1, 2, 3, 4, 5]
b=[3, 1, 4, 2, 5]
c=sorted(b)
print(b) # [3, 1, 4, 2, 5]
print(c) # [1, 2, 3, 4, 5]
以下是一些有用的學習資源,幫助你學習Python list:
總結
Python的list是一種非常靈活的數據結構,它可以存儲任何類型的元素,包括數字、字符串、甚至是其他的list。Python list提供了豐富的操作方法,包括訪問元素、添加元素、刪除元素、切片、排序等。我們希望通過這篇文檔提供了Python list的基礎知識和一些有用的學習資源,幫助你更好地掌握Python編程中的list。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。