电竞比分网-中国电竞赛事及体育赛事平台

分享

探索Java日志的奧秘: 底層日志系統(tǒng)

 蘇心閣 2019-10-07

log4j2是apache在log4j的基礎上,參考logback架構實現(xiàn)的一套新的日志系統(tǒng)(我感覺是apache害怕logback了)。

log4j2的官方文檔上寫著一些它的優(yōu)點:

在擁有全部logback特性的情況下,還修復了一些隱藏問題

API 分離:現(xiàn)在log4j2也是門面模式使用日志,默認的日志實現(xiàn)是log4j2,當然你也可以用logback(應該沒有人會這么做)

性能提升:log4j2包含下一代基于LMAX Disruptor library的異步logger,在多線程場景下,擁有18倍于log4j和logback的性能

多API支持:log4j2提供Log4j 1.2, SLF4J, Commons Logging and java.util.logging (JUL) 的API支持

避免鎖定:使用Log4j2 API的應用程序始終可以選擇使用任何符合SLF4J的庫作為log4j-to-slf4j適配器的記錄器實現(xiàn)

自動重新加載配置:與Logback一樣,Log4j 2可以在修改時自動重新加載其配置。與Logback不同,它會在重新配置發(fā)生時不會丟失日志事件。

高級過濾: 與Logback一樣,Log4j 2支持基于Log事件中的上下文數(shù)據(jù),標記,正則表達式和其他組件進行過濾。

插件架構: Log4j使用插件模式配置組件。因此,您無需編寫代碼來創(chuàng)建和配置Appender,Layout,Pattern Converter等。Log4j自動識別插件并在配置引用它們時使用它們。

屬性支持:您可以在配置中引用屬性,Log4j將直接替換它們,或者Log4j將它們傳遞給將動態(tài)解析它們的底層組件。

Java 8 Lambda支持

自定義日志級別

產生垃圾少:在穩(wěn)態(tài)日志記錄期間,Log4j 2 在獨立應用程序中是無垃圾的,在Web應用程序中是低垃圾。這減少了垃圾收集器的壓力,并且可以提供更好的響應時間性能。

和應用server集成:版本2.10.0引入了一個模塊log4j-appserver,以改進與Apache Tomcat和Eclipse Jetty的集成。

Log4j2類圖:

這次從四個地方去探索源碼:啟動,配置,異步,插件化

源碼探索

啟動

log4j2的關鍵組件

LogManager

根據(jù)配置指定LogContexFactory,初始化對應的LoggerContext

LoggerContext

1、解析配置文件,解析為對應的java對象。

2、通過LoggerRegisty緩存Logger配置

3、Configuration配置信息

4、start方法解析配置文件,轉化為對應的java對象

5、通過getLogger獲取logger對象

Logger

LogManaer

該組件是Log4J啟動的入口,后續(xù)的LoggerContext以及Logger都是通過調用LogManager的靜態(tài)方法獲得。我們可以使用下面的代碼獲取Logger

Logger logger = LogManager.getLogger;

可以看出LogManager是十分關鍵的組件,因此在這個小節(jié)中我們詳細分析LogManager的啟動流程。

LogManager啟動的入口是下面的static代碼塊:

這段靜態(tài)代碼段主要分為下面的幾個步驟:

首先根據(jù)特定配置文件的配置信息獲取loggerContextFactory

如果沒有找到對應的Factory的實現(xiàn)類則通過ProviderUtil中的getProviders方法載入providers,隨后通過provider的loadLoggerContextFactory方法載入LoggerContextFactory的實現(xiàn)類

如果provider中沒有獲取到LoggerContextFactory的實現(xiàn)類或provider為空,則使用SimpleLoggerContextFactory作為LoggerContextFactory。

根據(jù)配置文件載入LoggerContextFactory

在這段邏輯中,LogManager優(yōu)先通過配置文件”log4j2.component.properties”通過配置項”log4j2.loggerContextFactory”來獲取LoggerContextFactory,如果用戶做了對應的配置,通過newCheckedInstanceOf方法實例化LoggerContextFactory的對象,最終的實現(xiàn)方式為:

在默認情況下,不存在初始的默認配置文件log4j2.component.properties,因此需要從其他途徑獲取LoggerContextFactory。

通過Provider實例化LoggerContextFactory對象

代碼:

這里比較有意思的是hasProviders和getProviders都會通過線程安全的方式去懶加載ProviderUtil這個對象。跟進lazyInit方法:

再看構造方法:

這里的懶加載其實就是懶加載Provider對象。在創(chuàng)建新的providerUtil實例的過程中就會直接實例化provider對象,其過程是先通過getClassLoaders方法獲取provider的類加載器,然后通過loadProviders(classLoader);加載類。在providerUtil實例化的最后,會統(tǒng)一查找”META-INF/log4j-provider.properties”文件中對應的provider的url,會考慮從遠程加載provider。而loadProviders方法就是在ProviderUtil的PROVIDERS列表中添加對一個的provider。可以看到默認的provider是org.apache.logging.log4j.core.impl.Log4jContextFactory

LoggerContextFactory = org.apache.logging.log4j.core.impl.Log4jContextFactory

Log4jAPIVersion = 2.1.0

FactoryPriority= 10

很有意思的是這里懶加載加上了鎖,而且使用的是

lockInterruptibly這個方法。lockInterruptibly和lock的區(qū)別如下:

lock 與 lockInterruptibly比較區(qū)別在于:

lock 優(yōu)先考慮獲取鎖,待獲取鎖成功后,才響應中斷。

lockInterruptibly 優(yōu)先考慮響應中斷,而不是響應鎖的普通獲取或重入獲取。

ReentrantLock.lockInterruptibly允許在等待時由其它線程調用等待線程的

Thread.interrupt 方法來中斷等待線程的等待而直接返回,這時不用獲取鎖,而會拋出一個InterruptedException。 ReentrantLock.lock方法不允許Thread.interrupt中斷,即使檢測到Thread.isInterrupted,一樣會繼續(xù)嘗試獲取鎖,失敗則繼續(xù)休眠。只是在最后獲取鎖成功后再把當前線程置為interrupted狀態(tài),然后再中斷線程。

上面有一句注釋值得注意:

原來這里是為了讓osgi可以阻止啟動。

再回到logManager:

可以看到在加載完Provider之后,會做factory的綁定:

到這里,logmanager的啟動流程就結束了。

配置

在不使用slf4j的情況下,我們獲取logger的方式是這樣的:

Logger logger = logManager.getLogger(xx.class)

跟進getLogger方法:

這里有一個getContext方法,跟進,

上文提到factory的具體實現(xiàn)是Log4jContextFactory,跟進getContext

方法:

直接看start:

發(fā)現(xiàn)其中的核心方法是reconfigure方法,繼續(xù)跟進:

可以看到每一個configuration都是從ConfigurationFactory拿出來的,我們先看看這個類的getInstance看看:

這里可以看到ConfigurationFactory中利用了PluginManager來進行初始化,PluginManager會將ConfigurationFactory的子類加載進來,默認使用的XmlConfigurationFactory,JsonConfigurationFactory,YamlConfigurationFactory這三個子類,這里插件化加載暫時按下不表。

回到reconfigure這個方法,我們看到獲取ConfigurationFactory實例之后會去調用getConfiguration方法:

跟進getConfiguration,這里值得注意的是有很多個getConfiguration,注意甄別,如果不確定的話可以通過debug的方式來確定。

這里就會根據(jù)之前加載進來的factory進行配置的獲取,具體的不再解析。

回到reconfigure,之后的步驟就是setConfiguration,入參就是剛才獲取的config

這個方法最重要的步驟就是config.start,這才是真正做配置解析的

這里面有如下步驟:

獲取日志等級的插件

初始化

初始化Advertiser

配置

先看一下初始化,也就是setup這個方法,setup是一個需要被復寫的方法,我們以XMLConfiguration作為例子,

發(fā)現(xiàn)這里面有一個比較重要的方法constructHierarchy,跟進:

發(fā)現(xiàn)這個就是一個樹遍歷的過程。誠然,配置文件是以xml的形式給出的,xml的結構就是一個樹形結構。回到start方法,跟進doConfiguration:

發(fā)現(xiàn)就是對剛剛獲取的configuration進行解析,然后塞進正確的地方?;氐絪tart方法,可以看到昨晚配置之后就是開啟logger和appender了。

異步

AsyncAppender

log4j2突出于其他日志的優(yōu)勢,異步日志實現(xiàn)。我們先從日志打印看進去。找到Logger,隨便找一個log日志的方法。

一路跟進

可以看出這個在打日志之前做了調用次數(shù)的記錄。跟進tryLogMessage,

繼續(xù)跟進:

這里可以看到在實際打日志的時候,會從config中獲取打日志的策略,跟蹤ReliabilityStrategy的創(chuàng)建,發(fā)現(xiàn)默認的實現(xiàn)類為DefaultReliabilityStrategy,跟進看實際打日志的方法

這里實際打日志的方法居然是交給一個config去實現(xiàn)的。。。感覺有點奇怪。。跟進看看

可以清楚的看到try之前是在創(chuàng)建LogEvent,try里面做的才是真正的log(好tm累),一路跟進。

接下來就是callAppender了,我們直接開始看AsyncAppender的append方法:

這里主要的步驟就是:

生成logEvent

將logEvent放入BlockingQueue,就是transfer方法

如果BlockingQueue滿了則啟用相應的策略

同樣的,這里也有一個線程用來做異步消費的事情

直接看run方法:

阻塞獲取logEvent

將logEvent分發(fā)出去

如果線程要退出了,將blockingQueue里面的event消費完在退出。

AsyncLogger

直接從AsyncLogger的logMessage看進去:

跟進logWithThreadLocalTranslator,

這里的邏輯很簡單,就是將日志相關的信息轉換成RingBufferLogEvent(RingBuffer是Disruptor的無所隊列),然后將其發(fā)布到RingBuffer中。發(fā)布到RingBuffer中,那肯定也有消費邏輯。這時候有兩種方式可以找到這個消費的邏輯。

找disruptor被使用的地方,然后查看,但是這樣做會很容易迷惑

按照Log4j2的尿性,這種Logger都有對應的start方法,我們可以從start方法入手尋找

在start方法中,我們找到了一段代碼:

final RingBufferLogEventHandler[] handlers = {new RingBufferLogEventHandler};

disruptor.handleEventsWith(handlers);

直接看看這個RingBufferLogEventHandler的實現(xiàn):

順著接口找上去,發(fā)現(xiàn)一個接口:

通過注釋可以發(fā)現(xiàn),這個onEvent就是處理邏輯,回到RingBufferLogEventHandler的onEvent方法,發(fā)現(xiàn)里面有一個execute方法,跟進:

這個方法就是實際打日志了,AsyncLogger看起來還是比較簡單的,只是使用了一個Disruptor。

插件化

之前在很多代碼里面都可以看到

final PluginManager manager = new PluginManager(CATEGORY);

manager.collectPlugins(pluginPackages);

其實整個log4j2為了獲得更好的擴展性,將自己的很多組件都做成了插件,然后在配置的時候去加載plugin。

跟進collectPlugins。

處理邏輯如下:

從Log4j2Plugin.dat中加載所有的內置的plugin

然后將OSGi Bundles中的Log4j2Plugin.dat中的plugin加載進來

再加載傳入的package路徑中的plugin

最后加載配置中的plugin

邏輯還是比較簡單的,但是我在看源碼的時候發(fā)現(xiàn)了一個很有意思的東西,就是在加載log4j2 core插件的時候,也就是

PluginRegistry.getInstance.loadFromMainClassLoader

這個方法,跟進到decodeCacheFiles:

可以發(fā)現(xiàn)加載時候是從一個文件(PLUGIN_CACHE_FILE)獲取所有要獲取的plugin。看到這里的時候我有一個疑惑就是,為什么不用反射的方式直接去掃描,而是要從文件中加載進來,而且文件是寫死的,很不容易擴展啊。然后我找了一下PLUGIN_CACHE_FILE這個靜態(tài)變量的用處,發(fā)現(xiàn)了PluginProcessor這個類,這里用到了注解處理器。

/**

* Annotation processor for pre-scanning Log4j 2 plugins.

*/@SupportedAnnotationTypes('org.apache.logging.log4j.core.config.plugins.*')public class PluginProcessor extends AbstractProcessor { // TODO: this could be made more abstract to allow for compile-time and run-time plugin processing

(不太重要的方法省略)

我們可以看到在process方法中,PluginProcessor會先收集所有的Plugin,然后在寫入文件。這樣做的好處就是可以省去反射時候的開銷。

然后我又看了一下Plugin這個注解,發(fā)現(xiàn)它的RetentionPolicy是RUNTIME,一般來說PluginProcessor是搭配RetentionPolicy.SOURCE,CLASS使用的,而且既然你把自己的Plugin掃描之后寫在文件中了,RetentionPolicy就沒有必要是RUNTIME了吧,這個是一個很奇怪的地方。

小結

總算是把Log4j2的代碼看完了,發(fā)現(xiàn)它的設計理念很值得借鑒,為了靈活性,所有的東西都設計成插件式。互聯(lián)網技術日益發(fā)展,各種中間件層出不窮,而作為工程師的我們更需要做的是去思考代碼與代碼之間的關系,毫無疑問的是,解耦是最具有美感的關系。

    本站是提供個人知識管理的網絡存儲空間,所有內容均由用戶發(fā)布,不代表本站觀點。請注意甄別內容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權內容,請點擊一鍵舉報。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多