java 日誌體系(四)log4j 源碼分析
java 日誌體系(四)log4j 源碼分析
logback、log4j2、jul 都是在 log4j 的基礎上擴展的,其實現的邏輯都差不多,下面以 log4j 為例剖析一下日誌框架的基本組件。
一、總體架構
log4j 使用如下:
@Test
public void test() {
Log log = LogFactory.getLog(JclTest.class);
log.info("jcl log");
}
log.info 時調用的時序圖如下:
在 log4j 的配置文件,我們可以看到其三個最重要的組件:
Logger
每個 logger 可以單獨配置Appender
每個 appender 可以將日誌輸出到它想要的任何地方(文件、數據庫、消息等等)Layout
日誌格式布局
這三個組件的關系如下:
Log4j API(核心)
- 日誌對象(org.apache.log4j.Logger):供程序員輸出日誌信息
- 日誌附加器(org.apache.log4j.Appender):把格式化好的日誌信息輸出到指定的地方去
- ConsoleAppender - 目的地為控制臺的 Appender
- FileAppender - 目的地為文件的 Appender
- RollingFileAppender - 目的地為大小受限的文件的 Appender
- 日誌格式布局(org.apache.log4j.Layout):用來把程序員的 message 格式化成字符串
- PatternLayout - 用指定的 pattern 格式化 message的 Layout
- 日誌過濾器(org.apache.log4j.spi.Filter)
- 日誌事件(org.apache.log4j.LoggingEvent)
- 日誌級別(org.apache.log4j.Level)
- 日誌管理器(org.apache.log4j.LogManager)
- 日誌倉儲(org.apache.log4j.spi.LoggerRepository)
- 日誌配置器(org.apache.log4j.spi.Configurator)
- 日誌診斷上下文(org.apache.log4j.NDC、org.apache.log4j.MDC)
二、日誌管理器(org.apache.log4j.LogManager)
主要職責:
- 初始化默認 log4j 配置
- 維護日誌倉儲(org.apache.log4j.spi.LoggerRepository)
- 獲取日誌對象(org.apache.log4j.Logger)
2.1 初始化默認 log4j 配置
LogManager 的靜態代碼塊加載配置文件。
static {
// 1. 初始化默認的日誌倉庫 Hierarchy(實現了 LoggerRepository 接口)
// DefaultRepositorySelector#getLoggerRepository 簡單的封裝了 LoggerRepository
Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
repositorySelector = new DefaultRepositorySelector(h);
// 2. DEFAULT_CONFIGURATION_KEY=log4j.configuration 配置文件
// CONFIGURATOR_CLASS_KEY=log4j.configuratorClass 配置文件解析器,
// 分 DOMConfigurator 和 PropertyConfigurator 兩類
String configurationOptionStr = OptionConverter.getSystemProperty(
DEFAULT_CONFIGURATION_KEY, null);
String configuratorClassName = OptionConverter.getSystemProperty(
CONFIGURATOR_CLASS_KEY, null);
// 3. 根據配置文件路徑加載資源文件
URL url = null;
if (configurationOptionStr == null) {
url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE);
if (url == null) {
url = Loader.getResource(DEFAULT_CONFIGURATION_FILE);
}
} else {
try {
url = new URL(configurationOptionStr);
} catch (MalformedURLException ex) {
// so, resource is not a URL:
// attempt to get the resource from the class path
url = Loader.getResource(configurationOptionStr);
}
}
// 4. Configurator 解析配置文件
if (url != null) {
try {
OptionConverter.selectAndConfigure(url, configuratorClassName,
LogManager.getLoggerRepository());
} catch (NoClassDefFoundError e) {
LogLog.warn("Error during default initialization", e);
}
}
}
2.2 日誌倉儲(org.apache.log4j.spi.LoggerRepository)
主要職責:
- 管理日誌級別閾值(org.apache.log4j.Level)
- 管理日誌對象(org.apache.log4j.Logger)
LoggerRepository 的主要方法是 getLogger(name),創建一個日誌對象。
// ht 通過 key/value 的形式保存了所有的 logger,其中 key 為類的全路徑,value 為 logger
// logger 有父子關系,每個 logger 的父節點為前一個包名,如果父節點不存在則一直向上查找,直到 rootLogger
// 如果其父節點不存在,使用 ProvisionNode 先進行占位,ProvisionNode 保存有其全部的子節點
// 即 com.github.binarylei.log4j.Log4jTest1 的父節點為 com.github.binarylei.log4j,直到 rootLogger 為止
Hashtable ht;
public Logger getLogger(String name, LoggerFactory factory) {
CategoryKey key = new CategoryKey(name);
Logger logger;
synchronized (ht) {
Object o = ht.get(key);
// 1. 日誌倉庫中沒有創建一個
if (o == null) {
logger = factory.makeNewLoggerInstance(name);
logger.setHierarchy(this);
ht.put(key, logger);
updateParents(logger);
return logger;
// 2. 存在直接返回
} else if (o instanceof Logger) {
return (Logger) o;
// 3. ProvisionNode 占位用
} else if (o instanceof ProvisionNode) {
//System.out.println("("+name+") ht.get(this) returned ProvisionNode");
logger = factory.makeNewLoggerInstance(name);
logger.setHierarchy(this);
ht.put(key, logger);
// ProvisionNode 中的是子節點元素,logger 為當前的父節點
updateChildren((ProvisionNode) o, logger);
updateParents(logger);
return logger;
} else {
// It should be impossible to arrive here
return null;
}
}
}
其中有兩個相對比較重要的方法,updateParents 和 updateChildren
// 輪詢父節點,如果存在則直接指定其父節點
// 如果不存在則創建一個 ProvisionNode 用於占位,並設置 ProvisionNode 的子節點
final private void updateParents(Logger cat) {
String name = cat.name;
int length = name.length();
boolean parentFound = false;
// if name = "w.x.y.z", loop thourgh "w.x.y", "w.x" and "w", but not "w.x.y.z"
// 輪詢父節點
for (int i = name.lastIndexOf('.', length - 1); i >= 0;
i = name.lastIndexOf('.', i - 1)) {
String substr = name.substring(0, i);
CategoryKey key = new CategoryKey(substr); // simple constructor
Object o = ht.get(key);
// 1. 不存在父節點,創建一個 ProvisionNode 用於占位,設置其子節點為 cat
if (o == null) {
ProvisionNode pn = new ProvisionNode(cat);
ht.put(key, pn);
// 2. 存在父節點則指定當前 logger 的父節點
} else if (o instanceof Category) {
parentFound = true;
cat.parent = (Category) o;
break; // no need to update the ancestors of the closest ancestor
// 3. 如果是 ProvisionNode 直接添加其子節點
} else if (o instanceof ) {
((ProvisionNode) o).addElement(cat);
} else {
Exception e = new IllegalStateException("unexpected object type " +
o.getClass() + " in ht.");
e.printStackTrace();
}
}
// If we could not find any existing parents, then link with root.
if (!parentFound)
cat.parent = root;
}
// ProvisionNode 保存有當前 logger 的所有子節點
// 創建 logger 時如果找不到父節點則默認為 root,即 l.parent.name=root
// 如果 l.parent 已經是正確的父節點則忽略,否則就需要更新其父節點
final private void updateChildren(ProvisionNode pn, Logger logger) {
final int last = pn.size();
for (int i = 0; i < last; i++) {
Logger l = (Logger) pn.elementAt(i);
if (!l.parent.name.startsWith(logger.name)) {
logger.parent = l.parent;
l.parent = logger;
}
}
}
三、日誌對象(org.apache.log4j.Logger)
Logger 繼承自 org.apache.log4j.Priority。Logger 日誌級別: OFF、FATAL、ERROR、INFO、DEBUG、TRACE、ALL。
Logger 最終要的方法是輸出日誌,持有 Appender 才能輸出日誌。
3.1 Logger 管理 Appender
AppenderAttachableImpl 用來管理所有的 Appender,對 logger 上的所有 Appender 進行增刪改查,當前還一個最重要的方法 appendLoopOnAppenders 用於輸出日誌。
AppenderAttachableImpl aai;
public synchronized void addAppender(Appender newAppender) {
if (aai == null) {
aai = new AppenderAttachableImpl();
}
aai.addAppender(newAppender);
repository.fireAddAppenderEvent(this, newAppender);
}
3.2 Logger 日誌輸出
public void info(Object message) {
if (repository.isDisabled(Level.INFO_INT))
return;
if (Level.INFO.isGreaterOrEqual(this.getEffectiveLevel()))
forcedLog(FQCN, Level.INFO, message, null);
}
protected void forcedLog(String fqcn, Priority level, Object message, Throwable t) {
callAppenders(new LoggingEvent(fqcn, this, level, message, t));
}
callAppenders 最終調用 appender.doAppend(event) 進行日誌輸出。
public void callAppenders(LoggingEvent event) {
int writes = 0;
for (Category c = this; c != null; c = c.parent) {
// Protected against simultaneous call to addAppender, removeAppender,...
synchronized (c) {
// 1. 日誌輸出
if (c.aai != null) {
writes += c.aai.appendLoopOnAppenders(event);
}
// 2. 如果 logger.additive=false 則不會將日誌向上傳遞給父節點 logger
// 也就是說 additive=false 時日誌不會重復輸出,默認為 true
// 類似 spring 子容器的事件傳遞給父容器
if (!c.additive) {
break;
}
}
}
// 沒有日誌輸出
if (writes == 0) {
repository.emitNoAppenderWarning(this);
}
}
// AppenderAttachableImpl#appendLoopOnAppenders 用於日誌輸出
public int appendLoopOnAppenders(LoggingEvent event) {
int size = 0;
Appender appender;
if (appenderList != null) {
size = appenderList.size();
for (int i = 0; i < size; i++) {
appender = (Appender) appenderList.elementAt(i);
// 真正輸出日誌
appender.doAppend(event);
}
}
return size;
}
3.3 日誌事件(org.apache.log4j.LoggingEvent)
日誌事件是用於承載日誌信息的對象,其中包括:日誌名稱、日誌內容、日誌級別、異常信息(可選)、當前線程名稱、時間戳、嵌套診斷上下文(NDC)、映射診斷上下文(MDC)。
四、日誌附加器(org.apache.log4j.Appender)
日誌附加器是日誌事件(org.apache.log4j.LoggingEvent)具體輸出的介質,如:控制臺、文件系統、網絡套接字等。
日誌附加器(org.apache.log4j.Appender)關聯零個或多個日誌過濾器(org.apache.log4j.Filter),這些過濾器形成過濾鏈。
主要職責:
- 附加日誌事件(org.apache.log4j.LoggingEvent)
- 關聯日誌布局(org.apache.log4j.Layout)
- 關聯日誌過濾器(org.apache.log4j.Filter)
- 關聯錯誤處理器(org.apache.log4j.spi.ErrorHandler)
相關組件的關系如下,Append 持有 Layout、Filter、ErrorHandler。
4.1 Appender 主要流程
註意 logger#info 調用 doAppend 時加 synchronized 鎖了,所以是線程安全的,但了同時造成多線程時效率低下。所以才有了後來的 log4j2 和 logback 的出現。
public synchronized void doAppend(LoggingEvent event) {
// 1. 日誌級別判斷
if (!isAsSevereAsThreshold(event.getLevel())) {
return;
}
// 2. Filter 過濾
Filter f = this.headFilter;
FILTER_LOOP:
while (f != null) {
switch (f.decide(event)) {
// 1. 日誌事件跳過日誌附加器的執行
case Filter.DENY:
return;
// 2. 日誌附加器立即執行日誌事件
case Filter.ACCEPT:
break FILTER_LOOP;
// 3. 跳過當前過濾器,讓下一個過濾器決策
case Filter.NEUTRAL:
f = f.getNext();
}
}
// 3. 子類實現,日誌輸出
this.append(event);
}
doAppend 做日誌過濾,是否進行日誌輸出,真實的日誌輸出則直接委托給了 append 方法。append -> subAppend -> qw.write,QuietWriter 增加了對日誌輸出錯誤時的 ErrorHandler 處理。
public void append(LoggingEvent event) {
subAppend(event);
}
protected void subAppend(LoggingEvent event) {
this.qw.write(this.layout.format(event));
if (layout.ignoresThrowable()) {
String[] s = event.getThrowableStrRep();
if (s != null) {
int len = s.length;
for (int i = 0; i < len; i++) {
this.qw.write(s[i]);
this.qw.write(Layout.LINE_SEP);
}
}
}
if (shouldFlush(event)) {
this.qw.flush();
}
}
4.2 日誌過濾器(org.apache.log4j.spi.Filter)
日誌過濾器用於決策當前日誌事件(org.apache.log4j.spi.LoggingEvent)是否需要在執行所關聯的日誌附加器(org.apache.log4j.Appender)中執行。
決策結果有三種:
- DENY:日誌事件跳過日誌附加器的執行
- ACCEPT:日誌附加器立即執行日誌事件
- NEUTRAL:跳過當前過濾器,讓下一個過濾器決策
public void addFilter(Filter newFilter) {
if (headFilter == null) {
headFilter = tailFilter = newFilter;
} else {
tailFilter.setNext(newFilter);
tailFilter = newFilter;
}
}
4.3 Appender 類繼承關系
- ConsoleAppender - 目的地為控制臺的 Appender
- FileAppender - 目的地為文件的 Appender
- RollingFileAppender - 目的地為大小受限的文件的 Appender
WriterAppender 不關心日誌到底寫到那個流中,子類調用 createWriter 來創建一個具體的 Writer,這個 Writer 最終會被 QuietWriter 進行包裝。
// WriterAppender#createWriter
protected OutputStreamWriter createWriter(OutputStream os) {
OutputStreamWriter retval = null;
String enc = getEncoding();
if (enc != null) {
try {
retval = new OutputStreamWriter(os, enc);
} catch (IOException e) {
}
}
if (retval == null) {
retval = new OutputStreamWriter(os);
}
return retval;
}
4.3.1 FileAppender
FileAppender 通過 setFile 方法創建一個 QuietWriter 進行文件定入。
public synchronized void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize)
throws IOException {
if (bufferedIO) {
setImmediateFlush(false);
}
reset();
FileOutputStream ostream = null;
try {
ostream = new FileOutputStream(fileName, append);
} catch (FileNotFoundException ex) {
...
}
Writer fw = createWriter(ostream);
if (bufferedIO) {
fw = new BufferedWriter(fw, bufferSize);
}
this.setQWForFiles(fw);
this.fileName = fileName;
this.fileAppend = append;
this.bufferedIO = bufferedIO;
this.bufferSize = bufferSize;
writeHeader();
LogLog.debug("setFile ended");
}
4.3.2 RollingFileAppender 文件大小滾動
RollingFileAppender 根據文件大小進行滾動,有一個重要的屬性 maxFileSize 控制文件大小。RollingFileAppender#subAppend 每次寫日誌時都會判斷是否達到回滾的條件。
protected void subAppend(LoggingEvent event) {
super.subAppend(event);
if (fileName != null && qw != null) {
long size = ((CountingQuietWriter) qw).getCount();
if (size >= maxFileSize && size >= nextRollover) {
// 滾動生成新的日誌文件
rollOver();
}
}
}
4.3.3 DailyRollingFileAppender 時間滾動
DailyRollingFileAppender(根據時間滾動) 和 RollingFileAppender(根據文件大小滾動) 差不多,只是回滾的條件不一樣吧了。DailyRollingFileAppender 有一個重要的屬性 datePattern = "‘.‘yyyy-MM-dd" 用於控制多長時間滾動一次,具體配制規則見類註釋。
protected void subAppend(LoggingEvent event) {
long n = System.currentTimeMillis();
if (n >= nextCheck) {
now.setTime(n);
// 計算一次滾動的時間
nextCheck = rc.getNextCheckMillis(now);
try {
rollOver();
} catch (IOException ioe) {
...
}
}
super.subAppend(event);
}
五、日誌格式布局(org.apache.log4j.Layout)
日誌格式布局用於格式化日誌事件(org.apache.log4j.spi.LoggingEvent)為可讀性的文本內容。
Layout 最重要的方法是 format,將 LoggingEvent 轉換成可讀性的文本內容。
5.1 SimpleLayout
public String format(LoggingEvent event) {
sbuf.setLength(0);
sbuf.append(event.getLevel().toString());
sbuf.append(" - ");
sbuf.append(event.getRenderedMessage());
sbuf.append(LINE_SEP);
return sbuf.toString();
}
5.2 PatternLayout
PatternLayout 可以自定義 LoggingEvent 輸出格式,如 "%r [%t] %p %c %x - %m%n",初始化時會將 pattern 解析為 PatternConverter,PatternConverter 是一個鏈式結構。PatternLayout 自定義規則詳見 PatternLayout 類註釋。
public final static String DEFAULT_CONVERSION_PATTERN = "%m%n";
private StringBuffer sbuf = new StringBuffer(BUF_SIZE);
private String pattern;
private PatternConverter head;
public PatternLayout(String pattern) {
this.pattern = pattern;
head = createPatternParser((pattern == null) ? DEFAULT_CONVERSION_PATTERN :
pattern).parse();
}
protected PatternParser createPatternParser(String pattern) {
return new PatternParser(pattern);
}
LoggingEvent 格式化時調用 PatternConverter#format 方法,PatternConverter 具體格式化的實現以後有時間再看一下。
public String format(LoggingEvent event) {
// Reset working stringbuffer
if (sbuf.capacity() > MAX_CAPACITY) {
sbuf = new StringBuffer(BUF_SIZE);
} else {
sbuf.setLength(0);
}
PatternConverter c = head;
while (c != null) {
c.format(sbuf, event);
c = c.next;
}
return sbuf.toString();
}
六、日誌配置器(org.apache.log4j.spi.Configurator)
日誌配置器提供外部配置文件配置 log4j 行為的 API,log4j 內建了兩種實現:
- Properties 文件方式(org.apache.log4j.PropertyConfigurator)
- XML 文件方式(org.apache.log4j.xml.DOMConfigurator)
每天用心記錄一點點。內容也許不重要,但習慣很重要!
java 日誌體系(四)log4j 源碼分析