程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 為Eclipse插件添加日志框架

為Eclipse插件添加日志框架

編輯:關於JAVA

兩種增強Eclipse日志功能的方法

為什麼要采用日志?

良好的開發人員都知道精心設計、測試和調試的重要性。雖然 Eclipse 可以幫助開發人員實現這些任務,但是它怎樣處理日志呢?很多開發人員相信對於良好的軟件開發實踐來說,日志是不可或缺的一部分。如果您曾經修正過他人部署過的程序,您無疑也會同意這一點。幸運的是,日志對於性能的影響很小,大部分情況下甚至根本不會對性能產生任何影響,而且由於日志工具非常簡單易用,因此學習曲線也非常平滑。因此,對於現有的優秀工具,我們沒有理由不在應用程序中添加日志功能。

可以使用的工具

如果您正在編寫一個 Eclipse 插件,那麼您可以使用 org.eclipse.core.runtime.ILog 所提供的服務,它可以通過 Plug 類的 getLog() 方法進行訪問。只需要使用正確的信息創建一個 org.eclipse.core.runtime.Status 的實例,並調用 ILog 的 log() 方法即可。

這個日志對象可以接收多個日志監聽器實例。Eclipse 添加了兩個監聽器:

一個監聽器向 "Error Log(錯誤日志)" 視圖中寫入日志。

一個監聽器向位於 “${workspace}/.metadata/.log" 的日志文件中寫入日志。

您也可以創建自己的日志監聽器,只需實現 org.eclipse.core.runtime.ILogListener 接口並使用 addLogListener() 方法將其添加到日志對象中即可。這樣,每個日志事件都可以調用這個類的 logging() 方法。

雖然所有的內容都非常簡單,但是這種方法存在一些問題。如果您希望修改一個已部署好的插件目標,那麼應該如何處理?或者說要如何控制記錄下來的日志信息的數量?還有,這種實現可能會對性能造成影響,因為它總是要向所有的監聽器發送日志事件。這就是為什麼我們通常只在極端的情況(例如錯誤條件)中才會看到要記錄日志的原因。

另一方面,還有兩個專門用於日志的傑出的工具。一個來自 Java 2 SDK 1.4 的 java.util.logging 包;另外一個來自 Apache,名為 Log4j。

這兩個工具都采用了日志對象的層次結構的概念,都可以將日志事件發送到任意數目的處理程序(Handler,在 Log4j 中稱為 Appender)中,它代表了發送給格式化程序(Formatter,在 Log4j 中稱為 Layout)進行格式化的消息。這兩個工具都可以通過屬性文件進行配置。Log4j 還可以使用 xml 文件進行配置。

記錄器可以有一個名稱並與某一級別相關聯。記錄器可以繼承父母的設置(級別,處理程序)。名為“org”的記錄器會自動成為另外一個名為 “org.eclipse” 的記錄器的父母;因此不管您在配置文件中怎樣對“org”進行設置,這些設置都可以被“org.eclipse”記錄器繼承。

我更喜歡哪一個工具?這兩個工具我都曾經用過,不過我比較喜歡 Log4j。只有在非常簡單的程序中我才使用 java.util.logging,我並不想在這樣的程序中添加 log4j.jar。關於這兩個工具的詳細介紹,請參閱 Java 文檔和 Apache 的站點。

一種改進的日志

如果存在改進 Eclipse 日志體驗的方法,那不是很棒嗎?但這樣做有兩個問題:

缺少外部配置文件。

性能問題,同時還有缺乏對日志行為進行細粒度控制。

給出這個難題之後,我開始考慮將日志工具集成到 Eclipse 中的方法。我可以使用的第一個選擇是 java.util.logging,原因非常簡單:在 JSDK1.4 發行版中已經包含了這個包。

我想采用一個編輯器,通過配置文件對日志行為進行定制,從而允許將日志事件發送到任何可用的處理程序中。我計劃另外創建兩個處理程序:一個負責將日志事件發送到“Error Log”視圖中,另外一個將日志寫入插件所在的位置:“${workspace}/.metadata/.plugins /${plugin.name}"。

所有的內容都將包含在一個日志管理器插件(Plug-in Log Manager)中。您只能將其加入插件從屬關系中,並從中獲得日志對象。

然而,根據我的經驗,我不推薦使用 java.util.logging 來實現這項功能。因為實現的代碼將很長,而且只能保留一個 LogManager 實例;它使用系統類裝載程序來達到這個目的。這樣,所有的用戶只有一個層次結構,您會失去隔離性。因此,如果很多應用程序都在使用這個記錄器,那麼它們將共享設置,一個應用程序的記錄器實例可以繼承其他應用程序記錄器的設置。

既然如此,為什麼我們不對 LogManager 進行擴充,並自己實現一個記錄器呢?這種方法的問題是 LogManager 實例使用了系統類的裝載程序從配置文件中對類進行實例化。這種插件的優點之一是通過使用不同的類裝載程序提供隔離性。如果您的日志管理程序需要隔離性,那麼由於架構的限制, java.util.logging 可能不適合您的要求。

另一方面,Log4j 已經證明是非常有用的。不管您相信與否,Log4j 的記錄器的層次結構保留在一個稱為 Hierarchy 的對象中。因此,您可以為每個插件都創建一個層次結構,這樣問題就解決了。您還可以創建一個定制的 appender (處理程序)將事件發送給 "Error Log" 視圖,再創建一個將事件發送到插件所在的位置。這樣生活就變得美好起來了。

現在讓我們回顧一下整個過程是如何實現的,我們從插件編輯器的角度入手,創建插件,並將 com.tools.logging 添加到從屬類型列表中,然後創建一個 Log4j 配置文件。對 PluginLogManager 進行實例化,並使用配置文件對其進行配置。由於這個過程只需要做一次,因此您只需要在啟動插件時執行這項操作即可。對於日志語句,只需像在 Log4j 中那樣使用它即可。清單 1 給出了一個例子:

清單 1. TestPlugin 插件類中 PluginLogManager 的配置

private static final String LOG_PROPERTIES_FILE = "logger.properties";
public void start(BundleContext context) throws Exception {
   super.start(context);
   configure();
}
private void configure() {
   try {
    URL url = getBundle().getEntry("/" + LOG_PROPERTIES_FILE);
    InputStream propertiesInputStream = url.openStream();
    if (propertiesInputStream != null) {
      Properties props = new Properties();
      props.load(propertiesInputStream);
      propertiesInputStream.close();
      this.logManager = new PluginLogManager(this, props);
      this.logManager.hookPlugin( 
      TestPlugin.getDefault().getBundle().getSymbolicName(),
      TestPlugin.getDefault().getLog());
    }
   }
   catch (Exception e) {
    String message = "Error while initializing log properties." +
             e.getMessage();
    IStatus status = new Status(IStatus.ERROR,
    getDefault().getBundle().getSymbolicName(),
    IStatus.ERROR, message, e);
    getLog().log(status);
    throw new RuntimeException( 
       "Error while initializing log properties.",e);
   }
}

無論在何時部署插件,都只需要修改日志配置文件和日志過濾條件,或者修改其輸出,而不需要修改任何代碼。更好的一點是,如果日志被禁用,那麼所有的語句都不會影響性能,因為性能是 Log4j 設計的主要考慮因素之一。因此您可以在任何必要的地方采用這種記錄器的方法。

如何實現

對於 com.tools.logging 的使用,我們就談這麼多;現在讓我們來看一下其實現。

首先來看一下類 PluginLogManager。每個插件都有一個日志管理器。該管理器包含一個 hierarchy 對象,以及定制 appenders 所需的數據,如清單 2 所示。該對象並非直接源自於 Hierarchy 對象,因此不便將它暴露給最終用戶。它在實現方面提供了更多的自由。構造函數使用默認的 DEBUG 級別創建一個 hierarchy 對象,然後使用提供的屬性對其進行配置。它還可以簡單地使用 xml 屬性;只有對於對 Xerces 插件添加從屬性並使用 DOMConfigurator 而不是 PropertyConfigurator 才是必要的。這部分內容留給讀者作為練習。

清單 2. PluginLogManager 構造函數

public PluginLogManager(Plugin plugin,Properties properties) {
   this.log = plugin.getLog();
   this.stateLocation = plugin.getStateLocation();
   this.hierarchy = new Hierarchy(new RootCategory(Level.DEBUG));
   this.hierarchy.addHierarchyEventListener(new PluginEventListener());
   new PropertyConfigurator().doConfigure(properties,this.hierarchy);
   LoggingPlugin.getDefault().addLogManager(this);
}

注意 PluginLogManager 內部類是如何實現 org.apache.log4j.spi.HierarchyEventListener 的。這是向定制的 appender 傳遞必要信息的一種解決方案。在已經對 appender 進行實例化和完整配置並准備添加它時,會調用 addAppenderEvent() 方法,如清單 3 所示:

清單 3. PluginEventListener 類

private class PluginEventListener implements HierarchyEventListener {

   public void addAppenderEvent(Category cat, Appender appender) {
    if (appender instanceof PluginLogAppender) {
      ((PluginLogAppender)appender).setLog(log);
    }
    if (appender instanceof PluginFileAppender) {
      ((PluginFileAppender)appender).setStateLocation(stateLocation);
    }
   }

   public void removeAppenderEvent(Category cat, Appender appender) {
   }
}

為了更好地理解 appender 的生命周期以及一些決定,可以使用 UML 順序圖(UML Sequence Diagram)。圖 1 顯示了創建和配置 PluginFileAppender 實例的事件順序。

Figure 1. PluginFileAppender 配置順序圖

對於這個 appender 來說,我們對 org.apache.log4j.RollingFileAppender 進行了擴展。這不但允許您自由對文件進行操作,而且還提供了很多有用特性,例如文件大小的上限;當達到文件上限時,日志自動重疊寫入另一個文件。

通過選擇對 RollingFileAppender 進行擴展,您還需要對其行為進行正確處理。當 Log4j 創建 appender 之後,就會調用“setter”方法從配置文件中對其屬性進行初始化,然後調用 activateOptions() 方法讓附加程序完成未完成的任何初始化操作。在進行這項操作時, RollingFileAppender 實例會調用 setFile() ,它將打開日志文件並准備好寫入日志。只有此時 Log4j 才會通知 PluginEventListener 實例。

顯然,在有機會設置插件位置前,您不能打開文件。因此當調用 activateOptions() 時,如果還沒有位置信息,就會被標記為未決的;當最後設置位置信息時,會再次調用該方法,此時 appender 就准備好,可以使用了。

另外一個 appender PluginLogAppender 的生命周期相同,不過由於它並沒有對現有的 appender 進行擴展,因此您不必擔心初始化的問題。appender 在 addAppenderEvent 方法被調用之前不會啟動。Log4j 文檔對如何編寫定制 appender 進行了詳細的討論。清單 4 給出了 append 方法。

清單 4. PluginLogAppender 的 append 方法

public void append(LoggingEvent event) {

   if (this.layout == null) {
    this.errorHandler.error("Missing layout for appender " +
        this.name,null,ErrorCode.MISSING_LAYOUT);
    return;
   }
   String text = this.layout.format(event);
   Throwable thrown = null;
   if (this.layout.ignoresThrowable()) {
    ThrowableInformation info = event.getThrowableInformation();
    if (info != null)
      thrown = info.getThrowable();
   }

   Level level = event.getLevel();
   int severity = Status.OK;
   if (level.toInt() >= Level.ERROR_INT)
    severity = Status.ERROR;
   else
   if (level.toInt() >= Level.WARN_INT)
    severity = Status.WARNING;
   else
   if (level.toInt() >= Level.DEBUG_INT)
    severity = Status.INFO;

   this.pluginLog.log(new Status(severity,
        this.pluginLog.getBundle().getSymbolicName(),
        level.toInt(),text,thrown));
}

LoggingPlugin 類維護了 PluginLogManagers 的一個列表。這是必需的,這樣,在插件停止時,就可以關閉該插件的所有層次結構,並正確刪除 appender 和記錄器,如清單 5 所示。

清單 5. LoggingPlugin 類處理日志管理器

private ArrayList logManagers = new ArrayList();
public void stop(BundleContext context) throws Exception {
   synchronized (this.logManagers) {
    Iterator it = this.logManagers.iterator();
    while (it.hasNext()) {
      PluginLogManager logManager = (PluginLogManager) it.next();
      logManager.internalShutdown();
    }
    this.logManagers.clear();
   }
   super.stop(context);
}
void addLogManager(PluginLogManager logManager) {
   synchronized (this.logManagers) {
    if (logManager != null)
      this.logManagers.add(logManager);
   }
}

void removeLogManager(PluginLogManager logManager) {
   synchronized (this.logManagers) {
    if (logManager != null)
      this.logManagers.remove(logManager);
   }
}

插入 PluginLogManager 類的內容有很多。有時您所從屬的插件,特別是那些從屬於 workbench 的插件,可能引發異常。這些異常通常都會被 Eclipse 記錄到日志中。允許將從屬插件(dependent plug-in)插入日志框架中,這非常有用。在觸發異常時,Eclipse 要記錄的所有日志都會被放入日志框架,它與其他記錄器共享配置文件。這種方法非常有用,因為這樣可以將所有的內容都集中在一個位置上,並可以保留一個事實的歷史樣本,從而有助於修正應用程序的問題。

這可以通過實現 org.eclipse.core.runtime.ILogListener 並將其添加到從屬插件的 ILog 實例中實現。基本上,您只需要將其與 Eclipse 的日志相關聯。然後,這種實現就可以將所有的請求都重定向到一個使用您選擇的名字(通常是一個插件標識符)創建的記錄器中。然後您可以通過相同的配置文件對輸出結果進行配置;只需指定記錄器的名字、設置過濾條件、添加 appender 即可。該類如清單 6 所示:

清單 6. PluginLogListener 類

class PluginLogListener implements ILogListener {
   private ILog log;
   private Logger logger;
   PluginLogListener(ILog log,Logger logger) {
    this.log = log;
    this.logger = logger;
    log.addLogListener(this);
   }
   void dispose() {
    if (this.log != null) {
      this.log.removeLogListener(this);
      this.log = null;
      this.logger = null;
    }
   }
   public void logging(IStatus status, String plugin) {
    if (null == this.logger || null == status)
      return;

    int severity = status.getSeverity();
    Level level = Level.DEBUG;
    if (severity == Status.ERROR)
      level = Level.ERROR;
    else
    if (severity == Status.WARNING)
      level = Level.WARN;
    else
    if (severity == Status.INFO)
      level = Level.INFO;
    else
    if (severity == Status.CANCEL)
      level = Level.FATAL;
    plugin = formatText(plugin);
    String statusPlugin = formatText(status.getPlugin());
    String statusMessage = formatText(status.getMessage());
    StringBuffer message = new StringBuffer();
    if (plugin != null) {
      message.append(plugin);
      message.append(" - ");
    }
    if (statusPlugin != null &&
       (plugin == null || !statusPlugin.equals(plugin))) {
      message.append(statusPlugin);
      message.append(" - ");
    }
    message.append(status.getCode());
    if (statusMessage != null) {
      message.append(" - ");
      message.append(statusMessage);
    }
    this.logger.log(level,message.toString(),status.getException());
   }

   static private String formatText(String text) {
    if (text != null) {
      text = text.trim();
      if (text.length() == 0) return null;
    }
    return text;
   }
}

整個框架是在一個插件項目 com.tools.logging 中實現的。為了顯示它是如何工作的,我創建了兩個插件:

HelloPlugin是從一個項目模板中構建出來的,它顯示一個消息對話框,其中顯示 "Hello, Eclipse world"。

TestPluginLog 作為一個與 HelloPlugin 的一個從屬插件添加的,因此它可以被勾掛在相同的日志級別中。它有一個方法 dummyCall() ,可以使用 Eclipse API 添加一條假消息,然後它會被重定向到 HelloPlugin 的日志中。

其他插件的從屬類型都已經設置好了,例如 org.eclipse.ui 或 org.eclipse.core.runtime。

為了顯示 logger.properties 配置文件的強大功能,在創建該文件時我非常小心。正如您在清單 7 中看到的一樣,我們定義了兩個 appender:appender A1 是一個 PluginFileAppender 類,它被分配給根記錄器。其他記錄器都是從這個根記錄器繼承而來,都將使用這個 appender。因此,所有的日志,包括來自 TestPluginLog 插件的日志,都被寫入一個位於插件所在位置的文件中。

清單 7. HelloPlugin 項目中的 Logger.properties 文件

log4j.rootCategory=, A1
# A1 is set to be a PluginFileAppender
log4j.appender.A1=com.tools.logging.PluginFileAppender
log4j.appender.A1.File=helloplugin.log
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%p %t %c - %m%n
# A2 is set to be a PluginLogAppender
log4j.appender.A2=com.tools.logging.PluginLogAppender
log4j.appender.A2.layout=org.apache.log4j.PatternLayout
log4j.appender.A2.layout.ConversionPattern=%p %t %c - %m%n
# add appender A2 to helloplugin level only
log4j.logger.helloplugin=, A2

另外一個 appender 是 A2,它是一個 PluginLogAppender 類,只能將它添加到記錄器 "helloplugin" 中,因此 TestPluginLog 沒有使用它。否則,在 "Error View" 窗口中 "TestPluginLog" 就會有兩項:一個來自於 Eclipse,另外一個來自於 com.tools.logging。您可以自己做個實驗,然後就會明白我的意思了。只需將 A2 添加到 log4j.rootCategory 中並刪除 log4j.logger.helloplugin 所在的那個行即可。

清單 8 顯示了在點擊 "sample menu" 並顯示消息框之後 ${workspace}/.metadata/.plugins/HelloPlugin/helloplugin.log 的內容。注意 TestPluginLog Eclipse 日志是如何寫入最後一行中的。通過將您自己的日志和 Eclipse 插件日志寫入一個輸出文件中,可以保留日志事件的序列。

清單 8. helloplugin.log

INFO main helloplugin.actions.SampleAction - starting constructor.
INFO main helloplugin.actions.SampleAction - ending constructor.
WARN main helloplugin.actions.SampleAction - init
WARN main helloplugin.actions.SampleAction - run method
WARN main TestPluginLog - TestPluginLog - 0 - Logging using the Eclipse API.

結束語

本文介紹了兩種改進 Eclipse 日志功能的方法。一種方法是在插件中使用 com.tools.logging,這樣就可以使用 Log4j 中所有有用的特性;如果您願意的話,它依將是 Eclipse 日志框架的一部分。另外一種方法與一個插件相關,該插件並不了解 Log4j,但即時只使用 Eclipse 日志 API,也可以對其日志輸出進行配置。

實際上,您並不需要使用 com.tools.logging。現在,您可以展開示例代碼,並將其作為一個單獨的 jar 文件加入您自己的插件中。當然,不要忘記了 Log4j 的 jar 文件。

插件是使用新的 OSGI 創建的。所有的代碼都是使用 Eclipse 3.0 Release Candidate 1、Sun Java 2 SDK 1.4.2 和 Log4j 1.2.8 進行開發的,並在這些環境中進行了測試。在可以下載的代碼中,不包括 log4j-1.2.8.jar 文件。如果您要下載這些代碼,應該從 Apache 的 Log4j 中獲得這個 jar 文件,並在 com.tools.logging 項目和 com.tools.logging_1.0.0 插件目錄中包含該文件。

本文配套源碼

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