歡迎您光臨本站 註冊首頁

擴展JDK日誌框架

←手機掃碼閱讀     火星人 @ 2014-03-10 , reply:0

日誌記錄對於軟體的維護特別是對於已部署到運行環境之後的軟體調試都有著重要的意義.本文介紹了 JDK 的日誌框架,以及如何根據不同需求自定義日誌處理、消息格式化、消息級別等組件.最后闡述了如何利用 JDK 日誌框架的擴展能力將 Java 程序能夠通過 STAF(Software Testing Automation Framework,一種自動化測試框架)日誌服務來進行監視.

自 Java 1.4 開始,JDK 包括了一個嶄新的日誌框架包 java.util.logging,該日誌框架設計精良,和 JDK 緊密結合,控制靈活,使用簡單.日誌記錄對於軟體的維護特別是對於已部署到運行環境之後的軟體調試都有著重要的意義.在實際的項目中,往往還需要比該框架所提供的更為複雜的日誌功能.對於這種需求,JDK 日誌框架具有足夠的可擴展能力,可以自定義不同需求的日誌處理、消息格式化、日誌消息級別等組件.在下面的內容中,本文將介紹了如何擴展 JDK 日誌框架,自定義日誌處理方式.並就一個實際的例子來介紹如何結合 JDK 日誌框架和 STAF(Software Testing Automation Framework,一種自動化測試框架)日誌服務來對 Java 程序進行監視.

JDK 日誌框架介紹

JDK 的日誌框架即 java.util.logging 包.對於一個軟體的日誌系統而言,必須得有一個日誌對象,該對象負責記錄日誌信息.同時該信息可以輸出到不同的位置,例如控制台,文件甚至網路中.對於信息的格式,則可以根據不同的需求,可以輸出成普通文本,XML 或者 HTML 的格式.同時還需要對日誌信息進行不同級別的分類,這樣的好處是可以過濾冗餘信息,只保留關鍵的日誌.對於一個日誌框架而言,日誌對象必須是可配置的,它可以按照配置來輸出到指定的目標,同時按照配置來決定輸出的格式和決定何種級別以上的日誌才能輸出.配置的形式還可以是多種多樣的,既能是代碼的形式,也能是配置文件的形式.尤其是配置文件的形式,對於一個已經部署到運行環境中的軟體而言,可以非常方便的改變日誌配置而無需改變其源代碼.

JDK 日誌框架提供了上述的所有功能.它主要包括如下幾個部件:

Logger:日誌記錄對象.用於記錄日誌信息.
Handler:用於處理日誌信息的輸出.在 Handler 類中,可以決定日誌是輸出到文件中還是控制台中.
Filter: 用於過濾日誌.在 Filter 類中,可以根據日誌級別或者某種條件來決定是否輸出該日誌.這樣達到去除冗餘信息的目的.
Formatter:用于格式化日誌信息.該類可以將日誌文本格式化成 XML 或者 HTML 的格式,這完全依賴於具體的實現.
Level:用於表示日誌的級別. JDK 日誌框架默認有如下級別 : SEVERE,WARNING,INFO,CONFIG,FINE,FINER,FINEST .

對於程序而言,它的 Logger 對象會判斷日誌的級別是否滿足輸出級別的要求,然後將滿足級別要求的日誌消息交給所配置的 Handler 對象來處理,如果日誌對象配置了一個 Filter 對象,那麼 Filter 對象將會對日誌信息做一次過濾. Handler 對象接受到日誌消息后,根據其所配置的格式化類 Formatter 來改變日誌的格式,根據所配置的 Filter 對象和 Level 對象來再次過濾日誌信息,最后輸出到該種 Handler 對象所指定的輸出位置中,該輸出位置可以是控制台,文件,網路 socket 甚至是內存緩衝區.其架構模型如 圖 1 所示.

JDK 提供了如下幾種默認支持的 Handler 類:

ConsoleHandler: 輸出日誌到控制台中
FileHandler:輸出日誌到指定文件中
MemoryHandler:輸出日誌到內存緩衝區中,當一定的條件滿足的時候(如某種關鍵字的日誌信息)再將緩衝區中的日誌輸出
SocketHandler: 輸出日誌到網路 socket 中
StreamHandler: 輸出日誌到輸入輸出流對象中

同時 JDK 日誌框架也不失其靈活性,你可以定製自己所需要的 Handler,將日誌按照自定義的需求輸出到不同的位置,同時 Formatter,Level 類都可以自定義擴展,下面就詳細敘述如何自定義擴展這些組件.

自定義日誌 Handler

所有的 Handler 類都是繼承自 java.util.logging.Handler 抽象類,該類結構圖如 圖 2 所示.

圖 2 Handler 類圖

由該類圖可見,Handler 抽象類提供了抽象介面:publish, flush 和 close .這些介面提供了日誌輸出的基本功能.同時 Handler 類保存了 Formatter,Filter 和 Level 對象用來控制日誌輸出.因此,編寫自定義的 Handler 類需要如下步驟:

1、繼承 Handler 抽象類

2、實現 publish,flush 和 close 方法.其中 publish 方法是用於發布一條日誌記錄. flush 方法是清空內存緩衝區. close 方法是當應用程序關閉的時候,釋放該 Handler 類所申請的資源(如文件,socket 等)

3、設置默認的 Formatter,Filter 和 Level 對象.必要的時候,可以在類的初始化時候讀取配置文件來設置這些參數.

一個典型的自定義 Handler 類實現如清單 1 所示.

清單 1 自定義 Handler 類

public class MyHandler extends Handler {
private boolean doneHeader = false;


public MyHandler() {
setLevel(Level.INFO);
setFilter(null);
setFormatter(new SimpleFormatter());
}

[email protected]
public void close() throws SecurityException {
if (!doneHeader) {
output(getFormatter().getHead(this));
doneHeader = true;
}
output(getFormatter().getTail(this));
flush();
}

@Override
public void flush() {
// 清空緩衝區
}

@Override
public void publish(LogRecord record) {
if (!isLoggable(record)) {
return;
}
String msg = getFormatter().format(record);

try {
if (!doneHeader ) {
output(getFormatter().getHead(this));
doneHeader = true;
}
output(msg);
} catch (Exception ex) {
reportError(null, ex, ErrorManager.WRITE_FAILURE);
}
}

private void output(String message) {
// 實現日誌輸出
}
}

這裡 reportError 方法是將日誌類中的錯誤信息輸出到外界,這個是由 ErrorManager 類實現的,ErrorManager 類負責記錄日誌框架中 Handler 的錯誤,一般情況下是將該錯誤列印到控制台中.具體的每條日誌消息被 JDK 日誌框架封裝成 LogRecord 對象,該類部分定義如 清單 2 所示.

清單 2 LogRecord 類定義

public class LogRecord implements java.io.Serializable {
public String getLoggerName();
public void setLoggerName(String name);
public ResourceBundle getResourceBundle();
public void setResourceBundle(ResourceBundle bundle);
public Level getLevel();
public void setLevel(Level level);
public String getMessage();
public void setMessage(String message);
public Object[] getParameters();
public void setParameters(Object parameters[]);
public int getThreadID();
public void setThreadID(int threadID);
public long getMillis();
public void setMillis(long millis);
public Throwable getThrown();
public void setThrown(Throwable thrown);
...
}

由清單 2 可見,LogRecord 類包含了一個日誌消息的級別、消息文本、時間、參數、線程等等所有的信息,這些都交給 Handler,Formatter 和 Filter 這些對象來處理.同時該類也是可序列化的,可以序列化到網路和文件中.該類還可以和一個 ResourceBundle 對象綁定,實現消息字元串的本地化處理.

本節描述了一個典型的自定義的 Handler 類的實現.在本文後面部分將會有一個實際的例子來介紹如何實現一個 STAF 日誌處理類.

自定義日誌 Formatter

日誌可以被格式化為一定格式的文本,也可以成為 XML 或者 HTML 這樣標準的格式.這取決於 Formatter 類的具體實現. Formatter 抽象類提供了 format 成員函數用於擴展.一個典型的自定義 Formatter 類實現如清單 3 所示:

清單 3 LogRecord 類定義

public class MyFormatter extends Formatter {
private final String lineSeparator = System.getProperty("line.separator");
@Override
public String format(LogRecord record) {
StringBuffer sb = new StringBuffer();
String message = formatMessage(record);
sb.append(record.getLevel().getLocalizedName());
sb.append(message);
sb.append(lineSeparator);
if (record.getThrown() != null) {
try {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
record.getThrown().printStackTrace(pw);
pw.close();
sb.append(sw.toString());
} catch (Exception ex) {
}
}
return sb.toString();
}
}

其中 formatMessage 方法提供了默認的將日誌記錄本地化和格式化的方法.它還能支持 java.text 風格的文本格式化,這隻需要在調用 Logger 對象的 setMessage 方法設定 java.text 風格的格式字元串,同時通過 setParameters 方法設置參數,這樣 formatMessage 將會根據所設置的 java.text 風格的格式字元串來格式化日誌消息.總之,formatMessage 方法方便了子類格式化字元串.使子類只需要定義輸出文本的格式而無需考慮本地化等問題.

自定義日誌消息級別

JDK 日誌框架默認提供了 SEVERE,WARNING,INFO,CONFIG,FINE,FINER,FINEST 這幾種日誌級別.如果我們需要定義更多的日誌級別,只需要繼承 java.util.logging.Level 類,然後將自定義的級別作為靜態成員變數聲明即可.一個典型的自定義的消息類如清單 4 所示.

清單 4 自定義 Level 類

public class MyLevel extends Level {


protected MyLevel(String name, int value) {
super(name, value);
}

public static final Level Level1 = new MyLevel("Level1", 123);
... // 其他自定義級別
}

權重值 value 是一個整型數.在默認的 JDK 日誌級別中,SEVERE 的權重是 1000,FINEST 是 300,可以根據具體的需求來定義每個自定義級別的權重.例如在 WARNING 和 INFO 級別中加入一個新的級別,該級別的權重必須介於 800 到 900 之間.

自由的日誌配置

和其他日誌框架一樣,JDK 日誌框架同樣提供了強大的日誌配置功能.你既可以通過代碼進行動態配置,也可以通過配置文件來實現自由靈活的配置.通過代碼動態配置,應用程序可以實現在運行過程中改變日誌類的配置,動態地改變不同的配置組合.一個簡單的動態配置代碼如清單 5 所示.

清單 5 動態配置 Logger 對象

public static void main(String[] args){
Handler fh = new FileHandler("%t/wombat.log");
Logger.getLogger("logname").addHandler(fh);
Logger.getLogger("com.wombat").setLevel("com.wombat",Level.FINEST);
...
}

配置文件的配置方法則同樣靈活多變.它主要是在應用程序啟動時根據一個指定的配置文件來設置日誌對象.在配置文件中,日誌對象是由其名稱來標識的.一個典型的日誌配置文件如清單 6 所示.

清單 6 JDK Logger 配置文件

# 設置日誌對象的 Handler,日誌對象的名稱是 com.xyz.foo
com.xyz.foo.handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler

# 設置日誌對象的基本輸出級別
com.xyz.foo.level = INFO

#FileHandler 只允許輸出 SEVERE 以上級別的日誌
java.util.logging.ConsoleHandler.level = SEVERE

#ConsoleHandler 允許輸出 INFO 以上級別的日誌
java.util.logging.ConsoleHandler.level = INFO

當設置好一個日誌配置文件后,在 java 程序的啟動參數中,我們可以通過添加 -Djava.util.logging.config.file 參數來定義配置文件路徑,一個典型的 java 命令行如下:

java -Djava.util.logging.config.file=logger.properties -cp . Mainclass

我們也可以在應用程序中聲明自定義的 Handler,Formatter,Level 等組件,這隻需要這些自定義組件能夠在 classpath 中找到即可.

實例——結合 STAF 日誌服務

STAF(Software Testing Automation Framework)是一個自動化軟體測試框架,它可以實現分散式的自動化軟體測試管理.我們可以應用 STAF 庫的 Java API 來做基於 STAF 框架的應用,同時 STAF 同時也提供了日誌服務.其日誌服務是用來記錄自動化測試流程中的信息,方便在 24x7 的自動化測試中記錄自動化測試的操作,便於發現潛在的自動化測試管理腳本的問題.

既然我們可以用 STAF 的 Java API 來做基於 STAF 的應用,我們也可以將 JDK 的日誌框架同 STAF 的日誌服務介面結合起來. STAF 的日誌服務的 Java 介面定義如清單 7 所示:

清單 7 STAFLog 類定義

public class STAFLog
{
public STAFLog(String logType, String logName, STAFHandle handle);
public STAFResult log(int level, String msg)
// Log type constants
public static STAFResult log(STAFHandle theHandle, String logType,
String logName, int level, String msg)
public String getName();
public String getLogType();
public int getMonitorMask();
... //other methods
}

從清單 7 我們可以看出,STAFLog 類提供了方法可以將日誌信息存儲到 STAF 的日誌庫中, 這個日誌庫既可以是本地的文件,也可以是另一個 STAF 伺服器上的日誌庫.這是通過本地 STAF 伺服器的配置來決定的.而 STAFLog.log() 方法只用於記錄日誌信息.

將 STAF 日誌服務的 Java API 同 JDK 日誌框架結合起來需要做如下步驟:

創建 STAF 日誌 Handler 類

該類封裝了 STAF 日誌服務 API 的介面.同時 STAF 的 Java API 需要一個全局的 STAFHandle 對象,用來表示本地的 STAF 服務句柄.這個可以通過建立一個靜態的 STAFHandle 對象即可.其代碼如下所示,我們定義了一個 STAFHandler 類如清單 8 所示.

清單 8 STAFHandler 類實現

import java.util.logging.*;
import com.ibm.staf.wrapper.STAFLog;

public class STAFHandler extends Handler {
private String logName;
private static STAFHandle stafHandle = null;
public STAFHandler(String name) {
configure();
logName = name;
}
public STAFHandler() {
configure();
}

@Override
public void close() throws SecurityException {
if (stafHandle != null){
try {
stafHandle.unRegister();
} catch (STAFException e) {
//ignore
}
}
}

@Override
public void flush() {
//nothing
}

@Override
public void publish(LogRecord record) {
if (!isLoggable(record)) {
return;
}
String msg;
try {
msg = getFormatter().format(record);
} catch (Exception ex) {
reportError(null, ex, ErrorManager.FORMAT_FAILURE);
return;
}

try {
STAFLog.log(stafHandle, STAFLog.MACHINE,
logName, record.getLevel().getName(), msg);
} catch (Exception ex) {
reportError(null, ex, ErrorManager.WRITE_FAILURE);
}

...

在實現 STAFHandler 類時有以下幾個要點:

1、由於 STAF API 的調用時需要一個 STAFHandle 的對象來代表本地的 STAF 服務,在該類中聲明了一個全局變數用來存儲 STAFHandle .

2、close 方法是用來清理系統資源的,上述代碼的 close 方法中釋放了全局變數 STAFHandle 對象.

3、publish 方法就是獲得格式化后的消息后,直接調用 STAF 的日誌 API 將日誌發送到 STAF 服務中.

但到目前為止,我們還沒有給 STAFHandler 類添加一個配置的代碼,使之可以支持配置文件.下面我們定義了一個函數 configure,其代碼如清單 9 所示.

清單 9 配置函數實現

private void configure() {
if (stafHandle == null) {
try {
stafHandle = new STAFHandle("my application");
} catch (STAFException e) {
reportError("registe staf handle error", e, ErrorManager.OPEN_FAILURE);
}
}

LogManager manager = LogManager.getLogManager();
String cname = getClass().getName();
//set staf log name
logName = manager.getProperty(cname ".name");
if (logName == null)
logName = "demo.staflog";

//set formatter
String sformatter = manager.getProperty(cname ".formatter");
Formatter formatter = null;
if (sformatter != null) {
try {
formatter = (Formatter)Class.forName(sformatter).newInstance();
} catch (Exception e) {
//ignore
}
}

setFormatter(formatter == null? new STAFFormatter() : formatter);

//set level
String sLevel = manager.getProperty(cname ".level");
Level level = null;
if (sLevel != null) {
try {
level = STAFLevel.parse(sLevel);
} catch (Exception e) {
//ignore
}
}
setLevel(level == null? STAFLevel.DEBUG : level);
}

在實現配置文件支持的代碼中,有以下幾個要點:

1、STAF API 的初始化需要註冊 STAFHandle 對象.該註冊只能執行一次.我們根據全局變數 stafHandle 的值來決定是否註冊該對象.
2、JDK 的日誌框架有一個全局的 singleton 管理類 STAFManager,該類用於管理日誌類,並提供了讀取日誌配置文件的成員函數 getProperty .在上述的代碼中,我們通過 STAFManager.getProperty 方法,從日誌配置文件中讀取 STAFHandler 對象所設置的 Formatter 類名,然後通過反射生成一個新的 Formatter 對象,設置到 Handler 對象中.
3、對於日誌級別也是通過 STAFManager.getProperty 方法.需要注意的是由於我們的日誌級別是自定義的級別, Level 對象是由我們自定義的 Level 類 STAFLevel 來生成的.
4、我們也能定義自己需要的屬性.比如清單 9 中我們定義了一個 .name 屬性,用來存儲 STAF 日誌名稱,通過 getProperty 函數從配置文件中讀取 .name 屬性.

創建一個適合 STAF 日誌的 Formatter 類

由於 STAF 日誌服務無需特殊的格式,我們只需要定義一個普通文本格式的 Formatter 即可.其代碼如清單 10 所示,注意這裡考慮了如果記錄了一個異常對象的情況,將異常對象的 stack 列印到字元串中添加到消息文本中.

清單 10. STAFFormatter 實現

import java.io.*;
import java.util.logging.*;
public class STAFFormatter extends Formatter {
private final String lineSeparator = System.getProperty("line.separator");
@Override
public String format(LogRecord record) {
StringBuffer sb = new StringBuffer();
String message = formatMessage(record);
sb.append(message);
sb.append(lineSeparator);
if (record.getThrown() != null) {
try {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
record.getThrown().printStackTrace(pw);
pw.close();
sb.append(sw.toString());
} catch (Exception ex) {
}
}
return sb.toString();
}
}

創建對應於 STAF 日誌級別的 Level 對象

這是由於 STAFLog 有著不同的日誌消息級別,它包括 Fatal, Error, Warning, Info, Tracer, Debug 等級別,有些是 JDK 日誌框架已有的級別,有些則不是.我們需要增加新的 Level 對象來滿足 STAFLog 的需求.一個新的 Level 類:STAFLevel 定義如清單 11 所示.

清單 11 自定義 STAFLevel

import java.util.logging.Level;

public class STAFLevel extends Level {

protected STAFLevel(String name, int value) {
super(name, value);
}

protected STAFLevel(String name, int value, String resourceBundleName) {
super(name, value, resourceBundleName);
}

public static final Level FATAL = new STAFLevel("FATAL",980);
public static final Level ERROR = new STAFLevel("ERROR",980);
public static final Level TRACE = new STAFLevel("TRACE", 790);
public static final Level DEBUG = new STAFLevel("DEBUG", 690);
}

清單 11 定義了 FATAL,ERROR,TRACE 和 DEBUG 級別.這就和 STAFLog 中的部分級別一一對應起來了.

將一切組合起來

清單 12 描述了如何在一段實際的代碼中將 STAF 日誌處理類和 JDK 日誌類結合起來. 從清單 12 可以看出,該實例默認指定輸出到 STAF 日誌服務的日誌名稱為「 staflogger 」.然後通過動態配置的方法來設定 Handler,Level 和 Formatter .最后在調用 JDK 的日誌對象的 log 方法記錄了 4 種自定義級別的日誌.

清單 12 一個完整的例子

package demo.staflog;

import java.util.logging.Logger;

public class STAFLoggerTest {
public static void main(String[] args) {
Logger logger = Logger.getLogger(STAFLoggerTest.class.getName());
logger.setUseParentHandlers(false);
logger.setLevel(STAFLevel.DEBUG);
STAFHandler stafHandler = new STAFHandler("staflogger");
stafHandler.setLevel(STAFLevel.DEBUG);
stafHandler.setFormatter(new STAFFormatter());
logger.addHandler(stafHandler);
//log
logger.log(STAFLevel.DEBUG, "debug log");
logger.log(STAFLevel.FATAL, "fatal log");
logger.log(STAFLevel.ERROR, "error log");
logger.log(STAFLevel.TRACE, "trace log");
}
}

但我們也可以將這些代碼改為配置文件的方式,其配置文件如清單 13 所示:

清單 13 STAFLog 類定義

# 設置日誌對象的 Handler
demo.staflog.STAFLoggerTest.handlers= demo.staflog.STAFHandler
demo.staflog.STAFLoggerTest.level = DEBUG

# 取消發送日誌到父 Logger 對象
demo.staflog.STAFLoggerTest.useParentHandlers = FALSE

# 設置 Handler 的名稱,輸出級別和格式化對象
demo.staflog.STAFHandler.name= staflogger
demo.staflog.STAFHandler.level = DEBUG
demo.staflog.STAFHandler.formatter = demo.staflog.STAFFormatter

這樣代碼可以簡化為清單 14 .

清單 14 STAFLog 類定義

public class STAFLoggerTest {
private static Level defaultLevel = STAFLevel.DEBUG;
public static void main(String[] args) {
//log
logger.log(STAFLevel.DEBUG, "debug log");
logger.log(STAFLevel.FATAL, "fatal log");
logger.log(STAFLevel.ERROR, "error log");
logger.log(STAFLevel.TRACE, "trace log");
}
}

配置文件的方式相對於動態配置的方式更加靈活,這無需改變和重新編譯代碼,只需要修改配置文件,就能修改日誌中 Handler,Level 和 Formatter 的組合配置,這對於已經部署發布的軟體而言,有著更為實際的意義.

當運行代碼后,在命令行中輸入 STAF 命令來顯示 STAF 日誌 staflogger:

 mymachine:~ myname$ staf local log query machine mymachine logname staflogger     Response     --------     Date-Time         Level Message       ----------------- ----- ----------     20081111-16:15:21 Debug debug log     20081111-16:15:21 Fatal fatal log     20081111-16:15:21 Error error log     20081111-16:15:21 Trace trace log

這顯示了我們剛才在 Java 代碼中記錄的信息,它們已經被輸出到 STAF 的日誌服務中了.

結束語

JDK 日誌框架簡單靈活,它雖然比 log4j 出現的時期晚,但其功能並不比 log4j 少. JDK 日誌框架直接隸屬於 JDK,被 Java 標準所支持而無需安裝第三方庫文件.本文介紹了 JDK 日誌框架的結構,如何擴展 JDK 日誌框架使之滿足實際的項目需求.並以如何在 Java 程序中將日誌輸出到 STAF 的日誌服務中為例,一步步描述了如何實現擴展 JDK 日誌組件,使之和 STAF 日誌服務結合到一起,同時如何創建靈活的配置文件來組合日誌框架組件.希望本文可以給其他需要擴展 JDK 日誌組件的開發者提供幫助.

下載

描述 名字 大小 下載
本文代碼 demo.zip 13 KB 點擊

參考資料

Java Logging Overview, 這是 SUN 官方主頁上的關於 JDK Logging 框架的指南文章.概述了 JDK Logging 框架.


[火星人 ] 擴展JDK日誌框架已經有582次圍觀

http://coctec.com/docs/java/show-post-62102.html