(點選上方公眾號,可快速關註)
來源:盛江濤, 李思舒, 趙海兵 ,
www.ibm.com/developerworks/cn/java/j-lo-logbuffer/index.html
概述
日誌技術為產品的質量和服務提供了重要的支撐。JDK 在 1.4 版本以後加入了日誌機制,為 Java 開發人員提供了便利。但這種日誌機制是基於靜態日誌級別的,也就是在程式執行前就需設定下來要列印的日誌級別,這樣就會帶來一些不便。
在 JDK 提供的日誌功能中,日誌級別被細化為 9 級,用以區分不同日誌的用途,用來記錄一個錯誤,或者記錄正常執行的資訊,又或是記錄詳細的除錯資訊。由於日誌級別是靜態的,如果日誌級別設定過高,低階別的日誌難以打印出來,從而導致在錯誤發生時候,難以去追蹤錯誤的發生原因,目前常常採用的方式是在錯誤發生的時候,不得不先調整日誌級別到相對低的程度,然後再去觸發錯誤,使得問題根源得到顯現。但是這種發生問題需要改動產品配置,然後重新觸發問題進行除錯的方式使得產品使用者體驗變差,而且有些問題會因為偶發性,環境很複雜等原因很難重新觸發。
相反,如果起初就把日誌級別調整到比較低,那麼日誌中間會有大量無用資訊,而且當產品比較複雜的時候,會導致產生的日誌檔案很大,掃清很快,無法及時的記錄有效的資訊,甚至成為效能瓶頸,從而降低了日誌功能對產品的幫助。
本文藉助 Java Logging 中的 MemoryHandler 類將所有級別日誌快取起來,在適當時刻輸出,來解決這個問題。主要圍繞 MemoryHandler 的定義和 logging.properties 檔案的處理而展開。
實體依附的場景如下,設想使用者需要在產品發生嚴重錯誤時,檢視先前發生的包含 Exception 的錯誤資訊,以此作為診斷問題緣由的依據。使用 Java 緩衝機製作出的一個解決方案是,將所有產品執行過程中產生的包含 Exception 的日誌條目儲存在一個可設定大小的迴圈緩衝佇列中,當嚴重錯誤(SEVERE)發生時,將緩衝佇列中的日誌輸出到指定平臺,供使用者查閱。
Java 日誌機制的介紹
Java 日誌機制在很多文章中都有介紹,為了便於後面文章部分的理解,在這裡再簡單介紹一下本文用到的一些關鍵字。
Level:JDK 中定義了 Off、Severe、Warning、Info、Config、Fine、Finer、Finest、All 九個日誌級別,定義 Off 為日誌最高等級,All 為最低等級。每條日誌必須對應一個級別。級別的定義主要用來對日誌的嚴重程度進行分類,同時可以用於控制日誌是否輸出。
LogRecord:每一條日誌會被記錄為一條 LogRecord, 其中儲存了類名、方法名、執行緒 ID、列印的訊息等等一些資訊。
Logger:日誌結構的基本單元。Logger 是以樹形結構儲存在記憶體中的,根節點為 root。com.test(如果存在)一定是 com.test.demo(如果存在)的父節點,即字首匹配的已存在的 logger 一定是這個 logger 的父節點。這種父子關係的定義,可以為使用者提供更為自由的控制粒度。因為子節點中如果沒有定義處理規則,如級別 handler、formatter 等,那麼預設就會使用父節點中的這些處理規則。
Handler:用來處理 LogRecord,預設 Handler 是可以連線成一個鏈狀,依次對 LogRecord 進行處理。
Filter:日誌過濾器。在 JDK 中,沒有實現。
Formatter:它主要用於定義一個 LogRecord 的輸出格式。
圖 1 展示了一個 LogRecord 的處理流程。一條日誌進入處理流程首先是 Logger,其中定義了可透過的 Level,如果 LogRecord 的 Level 高於 Logger 的等級,則進入 Filter(如果有)過濾。如果沒有定義 Level,則使用父 Logger 的 Level。Handler 中過程類似,其中 Handler 也定義了可透過 Level,然後進行 Filter 過濾,透過如果後面還有其他 Handler,則直接交由後面的 Handler 進行處理,否則會直接系結到 formatter 上面輸出到指定位置。
在實現日誌快取之前,先對 Filter 和 Formatter 兩個輔助類進行介紹。
Filter
Filter 是一個介面,主要是對 LogRecord 進行過濾,控制是否對 LogRecord 進行進一步處理,其可以系結在 Logger 下或 Handler 下。
只要在 boolean isLoggable(LogRecord)方法中加上過濾邏輯就可以實現對 logrecord 進行控制,如果只想對發生了 Exception 的那些 log 記錄進行記錄,那麼可以透過清單 1 來實現,當然首先需要將該 Filter 透過呼叫 setFilter(Filter)方法或者配置檔案方式系結到對應的 Logger 或 Handler。
清單 1. 一個 Filter 實體的實現
@Override
public boolean isLoggable(LogRecord record){
if(record.getThrown()!=null){
return true;
}else{
return false;
}
}
Formatter
Formatter 主要是對 Handler 在輸出 log 記錄的格式進行控制,比如輸出日期的格式,輸出為 HTML 還是 XML 格式,文字引數替換等。Formatter 可以系結到 Handler 上,Handler 會自動呼叫 Formatter 的 String format(LogRecord r) 方法對日誌記錄進行格式化,該方法具有預設的實現,如果想實現自定義格式可以繼承 Formater 類並重寫該方法,預設情況下例如清單 2 在經過 Formatter 格式化後,會將 {0} 和 {1} 替換成對應的引數。
清單 2. 記錄一條 log
logger.log(Level.WARNING,”this log is for test1: {0} and test2:{1}”,
new Object[]{newTest1(),
new Test2()});
MemoryHandler
MemoryHandler 是 Java Logging 中兩大類 Handler 之一,另一類是 StreamHandler,二者直接繼承於 Handler,代表了兩種不同的設計思路。Java Logging Handler 是一個抽象類,需要根據使用場景建立具體 Handler,實現各自的 publish、flush 以及 close 等方法。
MemoryHandler 使用了典型的“註冊 – 通知”的觀察者樣式。MemoryHandler 先註冊到對自己感興趣的 Logger 中(logger.addHandler(handler)),在這些 Logger 呼叫釋出日誌的 API:log()、logp()、logrb() 等,遍歷這些 Logger 下系結的所有 Handlers 時,通知觸發自身 publish(LogRecord)方法的呼叫,將日誌寫入 buffer,當轉儲到下一個日誌釋出平臺的條件成立,轉儲日誌並清空 buffer。
這裡的 buffer 是 MemoryHandler 自身維護一個可自定義大小的迴圈緩衝佇列,來儲存所有執行時觸發的 Exception 日誌條目。同時在建構式中要求指定一個 Target Handler,用於承接輸出;在滿足特定 flush buffer 的條件下,如日誌條目等級高於 MemoryHandler 設定的 push level 等級(實體中定義為 SEVERE)等,將日誌移交至下一步輸出平臺。從而形成如下日誌轉儲輸出鏈:
在實體中,透過對 MemoryHandler 配置項 .push 的 Level 進行判斷,決定是否將日誌推向下一個 Handler,通常在 publish() 方法內實現。程式碼清單如下:
清單 3
// 只紀錄有異常並且高於 pushLevel 的 logRecord
final Level level = record.getLevel();
final Throwable thrown = record.getThrown();
If(level >= pushLevel){
push();
}
MemoryHandler.push 方法的觸發條件
Push 方法會導致 MemoryHandler 轉儲日誌到下一 handler,清空 buffer。觸發條件可以是但不侷限於以下幾種,實體中使用的是預設的第一種:
-
日誌條目的 Level 大於或等於當前 MemoryHandler 中預設定義或使用者配置的 pushLevel;
-
外部程式呼叫 MemoryHandler 的 push 方法;
-
MemoryHandler 子類可以多載 log 方法或自定義觸發方法,在方法中逐一掃描日誌條目,滿足自定義規則則觸發轉儲日誌和清空 buffer 的操作。MemoryHanadler 的可配置屬性
使用方式:
以上是記錄產品 Exception 錯誤日誌,以及如何轉儲的 MemoryHandler 處理的內部細節;接下來給出 MemoryHandler 的一些使用方式。
1. 直接使用 java.util.logging 中的 MemoryHandler
清單 4
// 在 buffer 中維護 5 條日誌資訊
// 僅記錄 Level 大於等於 Warning 的日誌條目並
// 掃清 buffer 中的日誌條目到 fileHandler 中處理
int bufferSize = 5;
f = new FileHandler(“testMemoryHandler.log”);
m = new MemoryHandler(f, bufferSize, Level.WARNING);
…
myLogger = Logger.getLogger(“com.ibm.test”);
myLogger.addHandler(m);
myLogger.log(Level.WARNING, “this is a WARNING log”);
2. 自定義
1)反射
思考自定義 MyHandler 繼承自 MemoryHandler 的場景,由於無法直接使用作為父類私有屬性的 size、buffer 及 buffer 中的 cursor,如果在 MyHandler 中有獲取和改變這些屬性的需求,一個途徑是使用反射。清單 5 展示了使用反射讀取使用者配置並設定私有屬性。
清單 5
int m_size;
String sizeString = manager.getProperty(loggerName + “.size”);
if (null != sizeString) {
try {
m_size = Integer.parseInt(sizeString);
if (m_size <= 0) {
m_size = BUFFER_SIZE; // default 1000
}
// 透過 java 反射機制獲取私有屬性
Field f;
f = getClass().getSuperclass().getDeclaredField(“size”);
f.setAccessible(true);
f.setInt(this, m_size);
f = getClass().getSuperclass().getDeclaredField(“buffer”);
f.setAccessible(true);
f.set(this, new LogRecord[m_size]);
} catch (Exception e) {
}
}
2)重寫
直接使用反射方便快捷,適用於對父類私有屬性無頻繁訪問的場景。思考這樣一種場景,預設環形佇列無法滿足我們儲存需求,此時不妨令自定義的 MyMemoryHandler 直接繼承 Handler,直接對儲存結構進行操作,可以透過清單 6 實現。
清單 6
public class MyMemoryHandler extends Handler{
// 預設儲存 LogRecord 的緩衝區容量
private static final int DEFAULT_SIZE = 1000;
// 設定緩衝區大小
private int size = DEFAULT_SIZE;
// 設定緩衝區
private LogRecord[] buffer;
// 參考 java.util.logging.MemoryHandler 實現其它部分
…
}
使用 MemoryHandler 時需關註的幾個問題
瞭解了使用 MemoryHandler 實現的 Java 日誌緩衝機制的內部細節和外部應用之後,來著眼於兩處具體實現過程中遇到的問題:Logger/Handler/LogRecord Level 的傳遞影響,以及如何在開發 MemoryHandler 過程中處理錯誤日誌。
1. Level 的傳遞影響
Java.util.logging 中有三種型別的 Level,分別是 Logger 的 Level,Handler 的 Level 和 LogRecord 的 Level. 前兩者可以透過配置檔案設定。之後將日誌的 Level 分別與 Logger 和 Handler 的 Level 進行比較,過濾無須記錄的日誌。在使用 Java Log 時需關註 Level 之間相互影響的問題,尤其在遍歷 Logger 系結了多個 Handlers 時。如圖 3 所示:
Java.util.logging.Logger 提供的 setUseParentHandlers 方法,也可能會影響到最終輸出終端的日誌顯示。這個方法允許使用者將自身的日誌條目列印一份到 Parent Logger 的輸出終端中。預設會列印到 Parent Logger 終端。此時,如果 Parent Logger Level 相關的設定與自身 Logger 不同,則列印到 Parent Logger 和自身中的日誌條目也會有所不同。如圖 4 所示:
2. 開發 log 介面過程中處理錯誤日誌
在開發 log 相關介面中呼叫自身介面列印 log,可能會陷入無限迴圈。Java.util.logging 中考慮到這類問題,提供了一個 ErrorManager 介面,供 Handler 在記錄日誌期間報告任何錯誤,而非直接丟擲異常或呼叫自身的 log 相關介面記錄錯誤或異常。Handler 需實現 setErrorManager() 方法,該方法為此應用程式構造 java.util.logging.ErrorManager 物件,併在錯誤發生時,透過 reportError 方法呼叫 ErrorManager 的 error 方法,預設將錯誤輸出到標準錯誤流,或依據 Handler 中自定義的實現處理錯誤流。關閉錯誤流時,使用 Logger.removeHandler 移除此 Handler 實體。
兩種經典使用場景,一種是自定義 MyErrorManager,實現父類相關介面,在記錄日誌的程式中呼叫 MyHandler.setErrorManager(new MyEroorManager()); 另一種是在 Handler 中自定義 ErrorManager 相關方法,示例如清單 7:
清單 7
public class MyHandler extends Handler{
// 在構造方法中實現 setErrorManager 方法
public MyHandler(){
……
setErrorManager (new ErrorManager() {
public void error (String msg, Exception ex, int code) {
System.err.println(“Error reported by MyHandler “
+ msg + ex.getMessage());
}
});
}
public void publish(LogRecord record){
if (!isLoggable(record)) return;
try {
// 一些可能會丟擲異常的操作
} catch(Exception e) {
reportError (“Error occurs in publish “, e, ErrorManager.WRITE_FAILURE);
}
}
……
}
logging.properties
logging.properties 檔案是 Java 日誌的配置檔案,每一行以“key=value”的形式描述,可以配置日誌的全域性資訊和特定日誌配置資訊,清單 8 是我們為測試程式碼配置的 logging.properties。
清單 8. logging.properties 檔案示例
#Level 等級 OFF > SEVERE > WARNING > INFO > CONFIG > FINE > FINER > FINEST > ALL
# 為 FileHandler 指定日誌級別
java.util.logging.FileHandler.level=WARNING
# 為 FileHandler 指定 formatter
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
# 為自定義的 TestMemoryHandler 指定日誌級別
com.ibm.test.MemoryHandler.level=INFO
# 設定 TestMemoryHandler 最多記錄日誌條數
com.ibm.test.TestMemoryHandler.size=1000
# 設定 TestMemoryHandler 的自定義域 useParentLevel
com.ibm.test.TestMemoryHandler.useParentLevel=WARNING
# 設定特定 log 的 handler 為 TestMemoryHandler
com.ibm.test.handlers=com.ibm.test.TestMemoryHandler
# 指定全域性的 Handler 為 FileHandler
handlers=java.util.logging.FileHandler
從 清單 8 中可以看出 logging.properties 檔案主要是用來給 logger 指定等級(level),配置 handler 和 formatter 資訊。
如何監聽 logging.properties
如果一個系統對安全性要求比較高,例如系統需要對更改 logging.properties 檔案進行日誌記錄,記錄何時何人更改了哪些記錄,那麼應該怎麼做呢?
這裡可以利用 JDK 提供的 PropertyChangeListener 來監聽 logging.properties 檔案屬性的改變。
例如建立一個 LogPropertyListener 類,其實現了 java.benas.PropertyChangeListener 介面,PropertyChangeListener 介面中只包含一個 propertyChange(PropertyChangeEvent)方法,該方法的實現如清 9 所示。
清單 9. propertyChange 方法的實現
@Override
public void propertyChange(PropertyChangeEvent event) {
if (event.getSource() instanceof LogManager){
LogManager manager=(LogManager)event.getSource();
update(manager);
execute();
reset();
}
}
propertyChange(PropertyChangeEvent)方法中首先呼叫 update(LogManager)方法來找出 logging.properties 檔案中更改的,增加的以及刪除的項,這部分程式碼如清單 10 所示;然後呼叫 execute() 方法來執行具體邏輯,參見 清單 11;最後呼叫 reset() 方法對相關屬性儲存以及清空,如 清單 12 所示。
清單 10. 監聽改變的條目
public void update(LogManager manager){
Properties logProps = null ;
// 使用 Java 反射機制獲取私有屬性
try {
Field f = manager.getClass().getDeclaredField(“props”);
f.setAccessible(true );
logProps=(Properties)f.get(manager);
}catch (Exception e){
logger.log(Level.SEVERE,”Get private field error.”, e);
return ;
}
Set
logPropsName=logProps.stringPropertyNames(); for (String logPropName:logPropsName){
String newVal=logProps.getProperty(logPropName).trim();
// 記錄當前的屬性
newProps.put(logPropName, newVal);
// 如果給屬性上次已經記錄過
if (oldProps.containsKey(logPropName)){
String oldVal = oldProps.get(logPropName);
if (newVal== null ?oldVal== null :newVal.equals(oldVal)){
// 屬性值沒有改變,不做任何操作
}else {
changedProps.put(logPropName, newVal);
}
oldProps.remove(logPropName);
}else {// 如果上次沒有記錄過該屬性,則其應為新加的屬性,記錄之
changedProps.put(logPropName, newVal);
}
}
}
程式碼中 oldProps、newProps 以及 changedProps 都是 HashMap
方法首先透過 Java 的反射機制獲得 LogManager 中的私有屬性 props(儲存了 logging.properties 檔案中的屬性資訊),然後透過與 oldProps 比較可以得到增加的以及修改的屬性資訊,最後 oldProps 中剩下的就是刪除的資訊了。
清單 11. 具體處理邏輯方法
private void execute(){
// 處理刪除的屬性
for (String prop:oldProps.keySet()){
// 這裡可以加入其它處理步驟
logger.info(“‘”+prop+”=”+oldProps.get(prop)+”‘has been removed”);
}
// 處理改變或者新加的屬性
for (String prop:changedProps.keySet()){
// 這裡可以加入其它處理步驟
logger.info(“‘”+prop+”=”+oldProps.get(prop)+”‘has been changed or added”);
}
}
該方法是主要的處理邏輯,對修改或者刪除的屬性進行相應的處理,比如記錄屬性更改日誌等。這裡也可以獲取當前系統的登入者,和當前時間,這樣便可以詳細記錄何人何時更改過哪個日誌條目。
清單 12. 重置所有資料結構
private void reset(){
oldProps = newProps;
newProps= new HashMap< String,String>();
changedProps.clear();
}
reset() 方法主要是用來重置各個屬性,以便下一次使用。
當然如果只寫一個 PropertyChangeListener 還不能發揮應有的功能,還需要將這個 PropertyChangeListener 實體註冊到 LogManager 中,可以透過清單 13 實現。
清單 13. 註冊 PropertyChangeListener
// 為’logging.properties’檔案註冊監聽器
LogPropertyListener listener= new LogPropertyListener();
LogManager.getLogManager().addPropertyChangeListener(listener);
如何實現自定義標簽
在 清單 8中有一些自定義的條目,比如 com.ibm.test.TestMemoryHandler。
useParentLever=WARNING”,表示如果日誌等級超過 useParentLever 所定義的等級 WARNING 時,該條日誌在 TestMemoryHandler 處理後需要傳遞到對應 Log 的父 Log 的 Handler 進行處理(例如將發生了 WARNING 及以上等級的日誌背景關係快取資訊列印到檔案中),否則不傳遞到父 Log 的 Handler 進行處理,這種情況下如果不做任何處理,Java 原有的 Log 機制是不支援這種定義的。那麼如何使得 Java Log 支援這種自定義標簽呢?這裡可以使用 PropertyListener 對自定義標簽進行處理來使得 Java Log 支援這種自定義標簽,例如對“useParentLever”進行處理可以透過清單 14 實現。
清單 14
private void execute(){
// 處理刪除的屬性
for (String prop:oldProps.keySet()){
if (prop.endsWith(“.useParentLevel”)){
String logName=prop.substring(0, prop.lastIndexOf(“.”));
Logger log=Logger.getLogger(logName);
for (Handler handler:log.getHandlers()){
if (handler instanceof TestMemoryHandler){
((TestMemoryHandler)handler)
.setUseParentLevel(oldProps.get(prop));
break ;
}
}
}
}
// 處理改變或者新加的屬性
for (String prop:changedProps.keySet()){
if (prop.endsWith(“.useParentLevel”)){
// 在這裡新增邏輯處理步驟
}
}
}
在清單 14 處理之後,就可以在自定義的 TestMemoryHandler 中進行判斷了,對 log 的等級與其域 useParentLevel 進行比較,決定是否傳遞到父 Log 的 Handler 進行處理。在自定義 TestMemoryHandler 中儲存對應的 Log 資訊可以很容易的實現將資訊傳遞到父 Log 的 Handler,而儲存對應 Log 資訊又可以透過 PropertyListener 來實現,例如清單 15 更改了 清單 13中相應程式碼實現這一功能。
清單 15
if (handler instanceof TestMemoryHandler){
((TestMemoryHandler)handler).setUseParentLevel(oldProps.get(prop));
((TestMemoryHandler)handler).addLogger(log);
break ;
}
具體如何處理自定義標簽的值那就看程式的需要了,透過這種方法就可以很容易在 logging.properties 新增自定義的標簽了。
自定義讀取配置檔案
如果 logging.properties 檔案更改了,需要透過呼叫 readConfiguration(InputStream)方法使更改生效,但是從 JDK 的原始碼中可以看到 readConfiguration(InputStream)方法會重置整個 Log 系統,也就是說會把所有的 log 的等級恢復為預設值,將所有 log 的 handler 置為 null 等,這樣所有儲存的資訊就會丟失。
比如,TestMemoryHandler 快取了 1000 條 logRecord,現在使用者更改了 logging.properties 檔案,並且呼叫了 readConfiguration(InputStream) 方法來使之生效,那麼由於 JDK 本身的 Log 機制,更改後對應 log 的 TestMemoryHandler 就是新建立的,那麼原來儲存的 1000 條 logRecord 的 TestMemoryHandler 實體就會丟失。
那麼這個問題應該如何解決呢?這裡給出三種思路:
1). 由於每個 Handler 都有一個 close() 方法(任何繼承於 Handler 的類都需要實現該方法),Java Log 機制在將 handler 置為 null 之前會呼叫對應 handler 的 close() 方法,那麼就可以在 handler(例如 TestMemoryHandler)的 close() 方法中儲存下相應的資訊。
2). 研究 readConfiguration(InputStream)方法,寫一個替代的方法,然後每次呼叫替代的方法。
3). 繼承 LogManager 類,改寫 readConfiguration(InputStream)方法。
這裡第一種方法是儲存原有的資訊,然後進行恢復,但是這種方法不是很實用和高效;第二和第三種方法其實是一樣的,都是寫一個替代的方法,例如可以在替代的方法中對 Handler 為 TestMemoryHandler 的不置為 null,然後在讀取 logging.properties 檔案時發現為 TestMemoryHandler 屬性時,找到對應 TestMemoryHandler 的實體,並更改相應的屬性值(這個在清單 14 中有所體現),其他不屬於 TestMemoryHandler 屬性值的可以按照 JDK 原有的處理邏輯進行處理,比如設定 log 的 level 等。
另一方面,由於 JDK1.6 及之前版本不支援檔案修改監聽功能,每次修改了 logging.properties 檔案後需要顯式呼叫 readConfiguration(InputStream)才能使得修改生效,但是自 JDK1.7 開始已經支援對檔案修改監聽功能了,主要是在 java.nio.file.* 包中提供了相關的 API,這裡不再詳述。
那麼在 JDK1.7 之前,可以使用 apache 的 commons-io 庫中的 FileMonitor 類,在此也不再詳述。
總結
透過對 MemoryHandler 和 logging.properties 進行定義,可以透過 Java 日誌實現自定義日誌快取,從而提高 Java 日誌的可用性,為產品質量提供更強有力的支援。
【關於投稿】
如果大家有原創好文投稿,請直接給公號傳送留言。
① 留言格式:
【投稿】+《 文章標題》+ 文章連結
② 示例:
【投稿】《不要自稱是程式員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/
③ 最後請附上您的個人簡介哈~
看完本文有收穫?請轉發分享給更多人
關註「ImportNew」,提升Java技能