程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java日志緩存機制的實現

Java日志緩存機制的實現

編輯:關於JAVA

概述

日志技術為產品的質量和服務提供了重要的支撐。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. Java 日志處理流程

圖 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) 等,將日志移交至下一步輸出平台。從而形成如下日志轉儲輸出鏈:

圖 2. Log 轉儲鏈

在實例 中,通過對 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 所示:

圖 3. Java Log 中 Level 的傳遞影響

Java.util.logging.Logger 提供的 setUseParentHandlers 方法,也可能會影響到最終輸出終端的日志 顯示。這個方法允許用戶將自身的日志條目打印一份到 Parent Logger 的輸出終端中。缺省會打印到 Parent Logger 終端。此時,如果 Parent Logger Level 相關的設置與自身 Logger 不同,則打印到 Parent Logger 和自身中的日志條目也會有所不同。如圖 4 所示:

圖 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<String> 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<String,String> 類型, oldProps 存儲修改前 logging.properties 文件內容,newProps 存儲修改後 logging.properties 內容, changedProps 主要用來存儲增加的或者是修改的部分。

方法首先通過 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 日志的可用性,為產品質量提供更強有力的支持。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved