摘要
上一篇《Logback日志框架初始化全過(guò)程源碼解析》分析了Logback的整個(gè)初始化過(guò)程以及Logger的創(chuàng)建您觉,這篇文章將繼續(xù)分析Logger的打印過(guò)程,上篇文章沒(méi)有介紹Logback的架構(gòu)熬荆,所以我們先會(huì)簡(jiǎn)單介紹下Logback的基礎(chǔ)組成模塊心傀;然后再介紹下logback.xml 配置文件中的主要元素和他們之間的協(xié)作關(guān)系,重點(diǎn)說(shuō)明下Logger的繼承特性爆捞;接著梳理下整個(gè)日志打印過(guò)程炎疆,帶著這些概念進(jìn)入源碼去一一驗(yàn)證卡骂,最后做個(gè)簡(jiǎn)單的總結(jié)。
正文
Logback基礎(chǔ)組成模塊
Logback主要分為3個(gè)模塊:
-
logback-core
core模塊是其他兩個(gè)模塊的基礎(chǔ)
-
logback-classic
classic模塊依賴core模塊形入,她是其實(shí)是log4j的升級(jí)版全跨,logback-classic天然實(shí)現(xiàn)了SLF4J API,所以我們可以在Log4j亿遂、Jul等日志框架之間隨意切換而不需要改動(dòng)我們的業(yè)務(wù)代碼浓若。
-
logback-access
access模塊主要用于和Servlet容器結(jié)合提供記錄HTTP請(qǐng)求日志的功能,通常不使用蛇数。
一般使用Logback時(shí)我們會(huì)引用slf4j-api挪钓、logback-core和logback-classic等3個(gè)依賴。
Logback配置
logback 提供多種配置方式:
-
通過(guò)實(shí)現(xiàn)
com.qos.logback.classic.spi.Configurator
接口來(lái)進(jìn)行配置上文《Logback日志框架初始化全過(guò)程源碼解析》中提到如果Logback初始化過(guò)程中如果找不到配置文件耳舅,且用戶也沒(méi)有實(shí)現(xiàn)Configurator接口碌上,則會(huì)使用默認(rèn)配置BasicConfigurator,她其實(shí)就實(shí)現(xiàn)了Configurator接口挽放。
通過(guò)加載配置文件
配置文件其實(shí)也提供2類:logback.grovy和logback.xml
兩者大同小異绍赛,只是語(yǔ)法不同而已,在這里我們選擇比較熟悉和常用的logback.xml進(jìn)行簡(jiǎn)單解釋辑畦。
<configuration>
<property name="pattern" value="%date %level [%thread] %logger{10} [%file : %line] %msg%n"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>${pattern}</Pattern>
</layout>
</appender>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${fileName:-demo.log}</file>
<encoder>
<pattern>${pattern}</pattern>
</encoder>
</appender>
<logger name="com" level="warn" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</logger>
<logger name="com.cw" level="info" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</logger>
<logger name="com.cw.demo" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</logger>
<root level="info">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
logback.xml中我們主要看6個(gè)xml元素:
appender
appender主要用來(lái)指定日志信息最終輸出到什么位置,console腿倚、file或者socket等纯出。
layout
layout用來(lái)根據(jù)pattern規(guī)則格式化日志內(nèi)容,將日志event轉(zhuǎn)換為String類型,然后通過(guò)java.io.Writer輸出暂筝。
encoder
encoder是0.9.19版本之后出現(xiàn)的箩言,她和layout作用本質(zhì)上是相同的,都會(huì)通過(guò)pattern規(guī)則將日志內(nèi)容格式化焕襟,不同點(diǎn)是陨收,encoder將日志event轉(zhuǎn)化為byte數(shù)組,然后寫(xiě)到OutputStream鸵赖。
0.9.19版本之后官方推薦encoder來(lái)代替layout务漩,具體原因,官方給出如下解釋:
Why the breaking change?
Layouts, as discussed in detail in the next chapter, are only able to transform an event into a String. Moreover, given that a layout has no control over when events get written out, layouts cannot aggregate events into batches. Contrast this with encoders which not only have total control over the format of the bytes written out, but also control when (and if) those bytes get written out.
意思是說(shuō)它褪,Layouts只能將日志event轉(zhuǎn)化為String饵骨,不能夠控制何時(shí)將日志event寫(xiě)出,無(wú)法將多個(gè)event集合到一組茫打。而encoder不僅能控制日志bytes數(shù)據(jù)的格式居触,還能控制是否以及何時(shí)寫(xiě)出數(shù)據(jù)。簡(jiǎn)單來(lái)講就是encoder比layout牛逼老赤,能用encoder就不用layout轮洋。當(dāng)然對(duì)于之前的使用layout的老代碼,官方也提供一些適配class例如LayoutWrappingEncoder
pattern
pattern主要用來(lái)定義日志的輸出格式等抬旺,詳見(jiàn)PatternLayout弊予。
logger
logger可以與我代碼中LoggerFactory.getLogger產(chǎn)生的Logger對(duì)象相對(duì)應(yīng),她有三個(gè)屬性:
name : 日志名稱嚷狞,用于Logger對(duì)象初始化的時(shí)候定位到配置文件中具體的配置块促。
-
level:標(biāo)識(shí)日志的有效等級(jí),日志等級(jí)具有繼承性床未。
例如:
logback.xml中name="com.cw"的logger(取別名Lc)等級(jí)是info竭翠, name="com.cw.demo"的logger(取別名為L(zhǎng)d),Lc是Ld的父級(jí)薇搁,而Ld沒(méi)有設(shè)置日志等級(jí)斋扰,那么她的等級(jí)就繼承子父級(jí)Lc的info,
如果Lc也沒(méi)有等級(jí)啃洋,那么會(huì)繼續(xù)往上找到name="com"這個(gè)祖先传货,繼承這個(gè)祖先的日志等級(jí)warn。
additivity:標(biāo)識(shí)該日志對(duì)象是否要將日志event向父級(jí)輸出宏娄,默認(rèn)為true问裕,一般我們會(huì)設(shè)置為false,防止一段日志信息被重復(fù)打印到多個(gè)日志文件中孵坚,無(wú)形中增加了日志文件的大小粮宛,稍后會(huì)結(jié)合源碼做分析窥淆。
appender-ref
appender-ref作用是將一個(gè)logger對(duì)象和具體的appender綁定,指定該logger將通過(guò)哪個(gè)appender將日志輸出到何處巍杈。一個(gè)logger可存在多個(gè)appender-ref忧饭,也就是說(shuō)可以同時(shí)綁定多個(gè)appender。
root
root其實(shí)是個(gè)特殊的logger筷畦,看名字就知道她是一個(gè)根節(jié)點(diǎn)词裤,是所有l(wèi)ogger的祖先,配置文件中必須得有一個(gè)root鳖宾。
日志打印源碼分析
過(guò)濾和創(chuàng)建LogEvent
這里我們以我們常用的logger.info() 為入口吼砂,info()中實(shí)際是調(diào)用filterAndLog_1()方法,
這個(gè)方法做2件事攘滩,過(guò)濾和創(chuàng)建LoggingEvent并推送給實(shí)際綁定的Appender對(duì)象
public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable {
public void info(String format, Object arg) {
filterAndLog_1(FQCN, null, Level.INFO, format, arg, null);
}
private void filterAndLog_1(final String localFQCN, final Marker marker, final Level level, final String msg, final Object param, final Throwable t) {
/**
* 這里先經(jīng)過(guò)一串過(guò)濾器處理帅刊,根據(jù)傳入的marker滿足的條件不同返回不同的結(jié)果,
* FilterReply.NEUTRAL 表示中立的意思
*/
final FilterReply decision = loggerContext.getTurboFilterChainDecision_1(marker, this, level, msg, param, t);
/**
* 過(guò)濾后返回NEUTRAL則進(jìn)一步判斷當(dāng)前想要打印的日志等級(jí)"INFO"是否高于或者等于有效日志級(jí)別漂问。
* 如果小于有效日志級(jí)別赖瞒,則表示該日志無(wú)效,直接返回蚤假,不做具體輸出動(dòng)作栏饮。
*/
if (decision == FilterReply.NEUTRAL) {
if (effectiveLevelInt > level.levelInt) {
return;
}
} else if (decision == FilterReply.DENY) {
return;
}
buildLoggingEventAndAppend(localFQCN, marker, level, msg, new Object[] { param }, t);
}
/**
* 創(chuàng)建日志Event, 推送到與logger綁定的所有appender中
*/
private void buildLoggingEventAndAppend(
final String localFQCN, final Marker marker,
final Level level, final String msg,
final Object[] params, final Throwable t) {
// 創(chuàng)建日志Event
LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
le.setMarker(marker);
//推送到與logger綁定的所有appender中
callAppenders(le);
}
public void callAppenders(ILoggingEvent event) {
int writes = 0;
/**
* 這里有個(gè)熟悉的單詞additive,上面我們介紹logger的時(shí)候說(shuō)到磷仰,
* 她是logger的一個(gè)屬性袍嬉,默認(rèn)為true
* 結(jié)合這段代碼我們看下,for循環(huán)中灶平,先將event, 推給當(dāng)前l(fā)ogger的Appenders中伺通,并且累加
* writes,緊接著判斷additive逢享,
* 如果為true則罐监,取logger的父級(jí),繼續(xù)將event推給父級(jí)logger的Appender中瞒爬,
* 以此類推弓柱,直至遇到某個(gè)父級(jí)或者祖先的additive為false,則break跳出循環(huán)侧但,
* 或者 送到root日志根節(jié)點(diǎn)為止矢空。
* 這幾解釋了additive的作用了。
*/
for (Logger l = this; l != null; l = l.parent) {
writes += l.appendLoopOnAppenders(event);
if (!l.additive) {
break;
}
}
// No appenders in hierarchy
if (writes == 0) {
loggerContext.noAppenderDefinedWarning(this);
}
}
private int appendLoopOnAppenders(ILoggingEvent event) {
if (aai != null) {
return aai.appendLoopOnAppenders(event);
} else {
return 0;
}
}
/**
* appenderList是與Logger綁定的Appender
* 例如上述logback.xml中name="FILE"的logger綁定的
* Appender 就是ch.qos.logback.core.FileAppender類禀横。
* 因?yàn)槊總€(gè)logger可以同時(shí)綁定多個(gè)Appender所以這里用數(shù)組appenderList來(lái)存儲(chǔ)這些Appender屁药。
*/
public int appendLoopOnAppenders(E e) {
int size = 0;
final Appender<E>[] appenderArray = appenderList.asTypedArray();
final int len = appenderArray.length;
for (int i = 0; i < len; i++) {
appenderArray[i].doAppend(e);
size++;
}
return size;
}
}
通過(guò)FileAppender打印到文件
logback.xml中name="FILE"的logger綁定的 Appender 是ch.qos.logback.core.FileAppender類,那么我們就以FileAppender為例分析下日志寫(xiě)入到文件的整個(gè)流程柏锄。
FIleAppender類的繼承關(guān)系圖:
整個(gè)流程如下:
FileAppender 指定日志打印的目標(biāo)文件者祖,并指定文件的輸出流OutputStream給OutputStreamAppender立莉,
目標(biāo)文件的指定動(dòng)作是發(fā)生在logger的初始化過(guò)程中绢彤,通過(guò)加載解析logback.xml找到name="FILE"的<appender>元素七问,讀取子元素<file>的值來(lái)獲取文件;
OutputStreamAppender 提供一個(gè)基礎(chǔ)服務(wù)茫舶,她繼承UnsynchronizedAppenderBase抽象類并實(shí)現(xiàn)其中的
append(E eventObject)
抽象方法械巡,進(jìn)行具體的日志格式化和打印動(dòng)作,其實(shí)就是向FileAppender提供的輸出流中輸入格式化后的日志饶氏;
通過(guò)名稱可看出UnsynchronizedAppenderBase并未做線程安全方面的措施讥耗,而是將線程安全方面的問(wèn)題交給子類自己去處理,OutputStreamAppender確實(shí)也自己處理了疹启,她通過(guò)在writeBytes(byte[] byteArray)
方法中加入 ReentrantLock
重入鎖來(lái)保證日志寫(xiě)入的線程安全古程。
接下來(lái)我們從下到上依次來(lái)分析下幾個(gè)類中重要的方法:
FileAppender
public class FileAppender<E> extends OutputStreamAppender<E> {
...
/**
*
*/
public void start() {
int errors = 0;
if (getFile() != null) {
...
/**
* 檢查該日志文件是否以及被其他Appender綁定了,如果沒(méi)被綁定則返回false
*/
if (checkForFileCollisionInPreviousFileAppenders()) {
addError("Collisions detected with FileAppender/RollingAppender instances defined earlier. Aborting.");
addError(MORE_INFO_PREFIX + COLLISION_WITH_EARLIER_APPENDER_URL);
errors++;
} else {
try {
/**
* 打開(kāi)未被其他Appender綁定的文件輸出流給OutputStreamAppender,
* 用于后續(xù)的日志輸入喊崖。
*/
openFile(getFile());
} catch (java.io.IOException e) {
errors++;
addError("openFile(" + fileName + "," + append + ") call failed.", e);
}
}
} else {
errors++;
addError("\"File\" property not set for appender named [" + name + "].");
}
if (errors == 0) {
super.start();
}
}
/**
* 檢查該日志文件是否以及被其他Appender綁定了挣磨,如果是的話就返回true
* 否則記錄appender的名稱和日志文件名稱,表示當(dāng)前文件已經(jīng)被Appender綁定了荤懂,
* 不能再和別的Appender綁定茁裙,然后返回false
*/
protected boolean checkForFileCollisionInPreviousFileAppenders() {
boolean collisionsDetected = false;
if (fileName == null) {
return false;
}
@SuppressWarnings("unchecked")
Map<String, String> map = (Map<String, String>) context.getObject(CoreConstants.FA_FILENAME_COLLISION_MAP);
if (map == null) {
return collisionsDetected;
}
for (Entry<String, String> entry : map.entrySet()) {
if (fileName.equals(entry.getValue())) {
addErrorForCollision("File", entry.getValue(), entry.getKey());
collisionsDetected = true;
}
}
if (name != null) {
map.put(getName(), fileName);
}
return collisionsDetected;
}
/**
* 創(chuàng)建個(gè)ResilientFileOutputStream輸出流,
* 給OutputStreamAppender中的节仿,outputStream成員變量晤锥;
* 文件必須是可寫(xiě)的;
* 雖然該方法是public的但是不要直接調(diào)用廊宪,
* 而是通過(guò)start方法一步步設(shè)置還成員變量的后再調(diào)用
*/
public void openFile(String file_name) throws IOException {
lock.lock();
try {
File file = new File(file_name);
boolean result = FileUtil.createMissingParentDirectories(file);
if (!result) {
addError("Failed to create parent directories for [" + file.getAbsolutePath() + "]");
}
ResilientFileOutputStream resilientFos = new ResilientFileOutputStream(file, append, bufferSize.getSize());
resilientFos.setContext(context);
setOutputStream(resilientFos);
} finally {
lock.unlock();
}
}
...
}
OutputStreamAppender
public class OutputStreamAppender<E> extends UnsynchronizedAppenderBase<E> {
protected Encoder<E> encoder;
protected final ReentrantLock lock = new ReentrantLock(false);
private OutputStream outputStream;
boolean immediateFlush = true;
@Override
protected void append(E eventObject) {
if (!isStarted()) {
return;
}
subAppend(eventObject);
}
/**
* 真正的的寫(xiě)入操作
* <p>
* Most subclasses of <code>WriterAppender</code> will need to override this
* method.
*/
protected void subAppend(E event) {
if (!isStarted()) {
return;
}
try {
// this step avoids LBCLASSIC-139
if (event instanceof DeferredProcessingAware) {
((DeferredProcessingAware) event).prepareForDeferredProcessing();
}
// 格式化日志文件
byte[] byteArray = this.encoder.encode(event);
// 寫(xiě)入日志到特定的輸出流中矾瘾,使用ReentrantLock保證線程安全
writeBytes(byteArray);
} catch (IOException ioe) {
this.started = false;
addStatus(new ErrorStatus("IO failure in appender", this, ioe));
}
}
/**
* 傳入一個(gè)已經(jīng)打開(kāi)的outputStream,
* 例如FileAppender 中打開(kāi)的ResilientFileOutputStream
*/
public void setOutputStream(OutputStream outputStream) {
lock.lock();
try {
// close any previously opened output stream
closeOutputStream();
this.outputStream = outputStream;
if (encoder == null) {
addWarn("Encoder has not been set. Cannot invoke its init method.");
return;
}
encoderInit();
} finally {
lock.unlock();
}
}
...
/**
* 寫(xiě)入日志到特定的輸出流中箭启,使用ReentrantLock保證線程安全
*/
private void writeBytes(byte[] byteArray) throws IOException {
if(byteArray == null || byteArray.length == 0)
return;
lock.lock();
try {
this.outputStream.write(byteArray);
if (immediateFlush) {
this.outputStream.flush();
}
} finally {
lock.unlock();
}
}
}
UnsynchronizedAppenderBase
abstract public class UnsynchronizedAppenderBase<E> extends ContextAwareBase implements Appender<E> {
protected boolean started = false;
/**
* guard 用來(lái)防止appender 重復(fù)調(diào)用自己的doAppend方法
*/
private ThreadLocal<Boolean> guard = new ThreadLocal<Boolean>();
/**
* Appenders are named.
*/
protected String name;
private FilterAttachableImpl<E> fai = new FilterAttachableImpl<E>();
private int statusRepeatCount = 0;
private int exceptionCount = 0;
static final int ALLOWED_REPEATS = 3;
public void doAppend(E eventObject) {
// 判斷當(dāng)前線程是否正在打印日志壕翩,如果不是才進(jìn)行打印動(dòng)作。
if (Boolean.TRUE.equals(guard.get())) {
return;
}
try {
guard.set(Boolean.TRUE);
if (!this.started) {
if (statusRepeatCount++ < ALLOWED_REPEATS) {
addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
}
return;
}
if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
return;
}
/**
* append是個(gè)抽象方法册烈,所以此處會(huì)調(diào)用append方法的具體實(shí)現(xiàn)
* 例如OutputStreamAppender類中實(shí)現(xiàn)的該方法
*/
this.append(eventObject);
} catch (Exception e) {
if (exceptionCount++ < ALLOWED_REPEATS) {
addError("Appender [" + name + "] failed to append.", e);
}
} finally {
guard.set(Boolean.FALSE);
}
}
abstract protected void append(E eventObject);
至此整個(gè)日志打印過(guò)程結(jié)束了戈泼。
總結(jié)
本文主要通過(guò)FileAppender
類為例詳細(xì)分析了整個(gè)日志的打印流程,除了FileAppender
還有一些我們常用的Appender赏僧,例如:
ConsoleAppender: 打印日志到控制臺(tái)
RollingFileAppender: 可以根據(jù)一定的規(guī)則對(duì)日志進(jìn)行切割備份大猛,如:日期、日志大小等規(guī)則
文中就不一一介紹了淀零,他們的流程都大同小異挽绩,留個(gè)感興趣的同學(xué)自己去分析。